> ## Documentation Index
> Fetch the complete documentation index at: https://rain-sandbox-trial.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Delivery

> How Rain delivers webhooks: the event lifecycle, responding to events, retries and idempotency, ordering, and versioning.

After your endpoint is live, Rain delivers each event as a signed `POST` request. This page explains how events move through their lifecycle, how to respond, the retry and delivery guarantees, how to handle out-of-order delivery, and how Rain versions payloads. To register an endpoint and verify signatures first, see [Set up webhooks](/docs/set-up-webhooks).

At a high level, every event follows the same delivery path from Rain to your endpoint.

<div className="wf-diagram">
  <div className="legend">
    <span className="lg"><span className="swatch send" />Signed request Rain POSTs</span>
    <span className="lg"><span className="swatch handle" />Delivered (2xx)</span>
    <span className="lg"><span className="swatch action" />Rain builds the event</span>
    <span className="lg"><span className="swatch cond" />Decision / no-op</span>
  </div>

  <div className="diagram-shell">
    <svg id="delivery" role="img" aria-label="How Rain delivers a webhook: Rain checks your subscription, signs the payload with HMAC-SHA256, POSTs it to your endpoint, and retries on any non-2xx response or timeout" viewBox="0 0 1080 320" width="1080" height="320" style={{width: "100%", height: "auto"}}><defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" className="marker-fill-default" /></marker><marker id="arrowSend" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" className="marker-fill-send" /></marker></defs><path d="M 196 130 L 236 130" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 412 130 L 452 130" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><text x="432" y="122" text-anchor="middle" className="phase-label">yes</text><path d="M 628 130 L 668 130" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><path d="M 844 130 L 884 130" fill="none" className="conn-send" stroke-width="1.7" stroke-dasharray="5 4" marker-end="url(#arrowSend)" opacity="0.92" /><path d="M 324 164 L 324 238" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><text x="338" y="206" text-anchor="start" className="phase-label">no</text><path d="M 726 164 L 726 206 L 786 206 L 786 166" fill="none" className="conn-default" stroke-width="1.7" marker-end="url(#arrow)" opacity="0.92" /><foreignObject x="20" y="96" width="176" height="68"><div className="card action"><span className="ct"><span className="tag">Trigger</span><span className="lab">Event occurs</span></span></div></foreignObject><foreignObject x="236" y="96" width="176" height="68"><div className="card cond"><span className="ct"><span className="tag">Check</span><span className="lab">Subscribed to this event?</span></span></div></foreignObject><foreignObject x="452" y="96" width="176" height="68"><div className="card action"><span className="ct"><span className="tag">HMAC-SHA256</span><span className="lab">Build & sign</span></span></div></foreignObject><foreignObject x="668" y="96" width="176" height="68"><div className="card send"><span className="ct"><span className="tag">Signed POST</span><span className="lab">Send to your endpoint</span></span></div></foreignObject><foreignObject x="884" y="96" width="176" height="68"><div className="card handle"><span className="ct"><span className="tag">2xx</span><span className="lab">Delivered</span></span></div></foreignObject><foreignObject x="236" y="238" width="176" height="64"><div className="card cond"><span className="ct"><span className="tag">Not subscribed</span><span className="lab">No webhook sent</span></span></div></foreignObject><foreignObject x="628" y="220" width="416" height="76"><div className="callout"><span className="co-t">↻ Retry on failure</span><span className="co-s">non-2xx or timeout: up to 15 retries, backoff capped at 1 day, give up after \~1 week</span></div></foreignObject></svg>
  </div>
</div>

## Webhook lifecycle

Most events follow a lifecycle as the underlying resource moves through its states. A spend transaction, for example, can move from `requested` to `created`, then `updated`, and finally `completed`. Not every event passes through every stage, and some events fire only once.

Each webhook payload shares the same envelope:

<CodeGroup>
  ```json Webhook envelope theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "resource": "transaction",
    "action": "completed",
    "version": "1.0.0",
    "eventReceivedAt": "2026-01-15T10:30:45.123Z",
    "body": {
      "id": "txn_123456"
    }
  }
  ```
</CodeGroup>

| Field             | Description                                                                                                                                           |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | Unique identifier for the event. Retries of the same event reuse this value, so you can use it to detect duplicates. See [Idempotency](#idempotency). |
| `resource`        | The resource that changed, such as `transaction` or `card`.                                                                                           |
| `action`          | The action that occurred, such as `created` or `completed`.                                                                                           |
| `version`         | Schema version of the `body` payload. Absent for some long-standing tenants, so treat it as optional when you parse. See [Versioning](#versioning).   |
| `eventReceivedAt` | Optional timestamp of when Rain received the underlying event. See [Handle event ordering](#handle-event-ordering).                                   |
| `body`            | The event data. Its structure depends on `resource` and `action`.                                                                                     |

## Respond to webhooks

Acknowledge each webhook quickly and keep your response lightweight:

* Return a JSON response with a `200` status code to acknowledge successful receipt
* Process heavy work asynchronously after you acknowledge, so slow handlers don't trigger timeouts and retries
* Include relevant information in your response if you need to communicate back to Rain (for example, [rejection codes](/docs/transaction#transaction-requested) for authorization webhooks)
* Avoid returning HTML error pages or plain text responses. If you do, Rain truncates and stores them, like this:

<CodeGroup>
  ```json Truncated response theme={null}
  {
    "truncated": true,
    "contentType": "text/html",
    "content": "<!DOCTYPE html><html><head><title>Error 500</title></head><body><h1>Internal Server Error</h1><p>An unexpected error occurred while processing your request. Please contact support if this issue persists.</p><div class=\"error-details\"><span>Error ID: abc123</span><span>Timestamp: 2026-12-16T10:30:00Z</span></div></body></html>... [truncated at 1000 characters]"
  }
  ```
</CodeGroup>

### Synchronous events

Three events are synchronous: Rain waits for your response before completing the action. Respond within the timeout shown below.

| Event                                                                      | Timeout | Your response                                                      |
| -------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------ |
| [`transaction.requested`](/docs/transaction#transaction-requested)         | 1500 ms | Return a `rejectionCode` in the body to decline the authorization. |
| [`challenge.requested`](/docs/card#challenge-requested)                    | 60 s    | Deliver the one-time passcode to the cardholder.                   |
| [`raindrop_balance.requested`](/docs/raindrops#raindrop_balance-requested) | 1500 ms | Return the user's `availableBalance` in the body.                  |

<Warning>
  Respond to `transaction.requested` within the 1500 ms timeout. Slower responses can cause the authorization to fail, surfacing `404` or `402` errors. Keep this handler fast and move heavy processing to a background job.
</Warning>

## Retry behavior and delivery guarantees

Rain automatically retries failed webhook deliveries. A delivery is considered failed if your endpoint does not return a `2xx` status code, times out, or is unreachable. Because Rain retries, your endpoint may receive the same event more than once, and events may arrive out of order. Design your handler to be idempotent and to tolerate out-of-order delivery (see [Handle event ordering](#handle-event-ordering)).

### How retries work

This schedule applies to asynchronous events only. When a delivery fails, Rain retries 15 times with exponential backoff for up to one week. Rain starts with a 2.5s delay for the first retry, and backs off with a coefficient of 3 (so the next retry is 7.5s, then 22.5s, and so on). Rain caps it at a day between retries.

[Synchronous events](#synchronous-events) are never retried. If `transaction.requested`, `challenge.requested`, or `raindrop_balance.requested` times out, the authorization or challenge it belongs to fails. There is no second delivery to catch, so a missed authorization decision is final.

### Idempotency

Webhooks may be delivered multiple times, so your handler should be idempotent. Each webhook payload includes a unique `id` field that you can use to detect duplicates. Store processed webhook IDs and check for duplicates before processing.

## Handle event ordering

Rain webhooks may arrive at your endpoint out of chronological order due to network delays, retries, or processing variations. The `eventReceivedAt` field in webhook payloads indicates **when the underlying event was received by Rain's system**, so you can determine the true chronological order of events even if webhooks arrive out of sequence.

### Understand `eventReceivedAt`

`eventReceivedAt` is an optional ISO-8601 timestamp field (format: `YYYY-MM-DDTHH:mm:ss.sssZ`) that indicates when Rain first received the underlying event that triggered the webhook.

<CodeGroup>
  ```json Webhook payload theme={null}
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "resource": "transaction",
    "action": "completed",
    "version": "1.0.0",
    "eventReceivedAt": "2026-01-15T10:30:45.123Z",
    "body": {
      "id": "txn_123456",
      "type": "spend",
      "spend": {
        "amount": 5000,
        "currency": "usd",
        "postedAt": "2026-01-15T10:30:50.000Z"
      }
    }
  }
  ```
</CodeGroup>

### Best practices for ordering

**Check for `eventReceivedAt`**: The field is optional and may not be present in all webhooks. Always check for its presence before using it.

**Use UTC timezone**: `eventReceivedAt` is always in UTC (indicated by the `Z` suffix). Ensure your parsing and comparison logic uses UTC and doesn't convert to local time.

**Consider a buffer window**: For more robust implementations, use a buffer window (for example, 30 to 60 seconds) before processing webhooks to account for late arrivals.

### Availability

For `transaction.requested`, `transaction.created`, `transaction.updated`,
`transaction.completed`, `user.updated`, and `company.updated`,
`eventReceivedAt` is opt-in and excluded by default. Contact your Rain
account manager to enable it for your tenant.

<Info>
  Some other events, such as `raindrop_balance.requested`,
  `dispute.chargebackCreated`, and `transactionReward.created`, show
  `eventReceivedAt` in their example payloads without this opt-in step.
  Confirm with your Rain account manager which of your subscribed events
  include the field by default versus which require enabling it.
</Info>

## Versioning

Each webhook payload is versioned. The `version` field in the envelope reflects the schema version of that event's `body`. Rain adds fields in a backward-compatible way, so new optional fields can appear in a payload without a breaking version change.

The version you receive is pinned by your webhook configuration. If you never set a version for an event, Rain sends that event's **default version, which is its earliest** — not its latest. To receive fields added in later versions, set the version for that event with the [webhook configuration endpoints](/reference/webhooks/update-webhook-configuration). Requesting a version that doesn't exist falls back to the default rather than failing.

For the change history of each event, review the changelog entries for that event, which record what changed in each version. Check them before you rely on a recently added field, and treat any field documented as optional as one that may be absent from a given payload.

## What's next

<Columns cols={2}>
  <Card title="Set up webhooks" icon="gear" href="/docs/set-up-webhooks">
    Register your endpoint, choose a signing key, and verify signatures.
  </Card>

  <Card title="Transaction events" icon="money-bill-transfer" href="/docs/transaction">
    See the payloads, fields, and triggers for spend, collateral, payment, and transfer events.
  </Card>
</Columns>
