> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polygon.technology/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook events

> The catalog of webhook events OMS delivers, and the envelope every event arrives in.

OMS delivers webhooks for every meaningful state change. Subscribe with `POST /webhooks`, passing a `url` and the `subscriptions` you want. For building the endpoint that consumes these events, see [Receiving webhooks](/payments/guides/receiving-webhooks). The create response returns a one-time `signingKey`; store it immediately, since it is never returned again. Verify every delivery with the `Webhook-Signature` header before processing, and deduplicate on the envelope's `id` so a redelivered event is handled once.

## Choosing your subscriptions

The `subscriptions` array accepts concrete event names from the catalog below, or the `*` wildcard.

| Selector              | Covers                                            |
| --------------------- | ------------------------------------------------- |
| `*`                   | Every partner-visible event in the catalog below. |
| A concrete event name | That event only.                                  |

<Note>
  `*` does not cover the [compliance review events](#compliance-review-events). Those are excluded from the wildcard by design and must be named explicitly.
</Note>

`POST /webhooks/{webhookId}/test` sends a `webhook.test` event to your endpoint. It is delivered regardless of your subscriptions, so you can verify signing and connectivity before any real event fires.

## Event envelope

A delivery carries the resource that changed under `data`, so you rarely need a follow-up read. Most events use the shape below.

| Field        | Type      | Description                                                                                                                  |
| ------------ | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `id`         | string    | The delivery ID (`whd_`). One per event and endpoint, stable across that delivery's retries. This is your deduplication key. |
| `event`      | string    | The event type from the catalog below.                                                                                       |
| `sequence`   | integer   | Per-resource monotonic counter. Use it to order events about the same resource and to detect gaps. Optional.                 |
| `createdAt`  | timestamp | When the delivery was created.                                                                                               |
| `occurredAt` | timestamp | When the event itself occurred. Optional.                                                                                    |
| `data`       | object    | The full resource object at the time of the event.                                                                           |

```json theme={null}
{
  "id": "whd_01H9Xz4M2k7vP3qR8t",
  "event": "virtualAccount.deposit.settled",
  "sequence": 1,
  "createdAt": "2026-07-08T12:00:02Z",
  "occurredAt": "2026-07-08T12:00:00Z",
  "data": { "id": "va_01H9Xd...", "object": "virtualAccount", "status": "active", "...": "..." }
}
```

The resource's `status` lives inside `data`, not on the envelope: branch on `data.status` (or the event name) rather than expecting an envelope-level status field.

### The `*.statusChanged` events

The five `*.statusChanged` events (`transaction`, `virtualAccount`, `depositAddress`, `externalAccount`, and `counterparty`) use a variant of this shape. It names the event type in `eventType` rather than `event`, and adds two fields:

| Field            | Type   | Description                                                                                                            |
| ---------------- | ------ | ---------------------------------------------------------------------------------------------------------------------- |
| `eventType`      | string | The event type. Replaces `event` on these five events.                                                                 |
| `eventId`        | string | The producer event ID (`evt_`), shared across the fan-out to every subscribed endpoint. Deduplicate on `id`, not this. |
| `previousStatus` | string | The status the resource moved out of. Optional.                                                                        |

```json theme={null}
{
  "id": "whd_01H9Xz4M2k7vP3qR8t",
  "eventId": "evt_01H9Xw2...",
  "eventType": "transaction.statusChanged",
  "sequence": 2,
  "createdAt": "2026-07-08T12:00:02Z",
  "occurredAt": "2026-07-08T12:00:00Z",
  "previousStatus": "processing",
  "data": { "id": "txn_01H9Xd...", "object": "transaction", "status": "completed", "...": "..." }
}
```

<Tip>
  Write your handler to read `event ?? eventType`. That one line covers both shapes and every event in the catalog.
</Tip>

## Transactions

Transactions emit a single event on every status transition. The full transaction arrives under `data`, so branch on `data.status`, using `data.subStatus` for the detail that distinguishes cases such as a payout held for review. The envelope's `previousStatus` tells you which status the transaction left.

| Event                       | Fires when                                                                                                                 |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `transaction.statusChanged` | A transaction changed status, in either direction and on any rail. Refund transitions on a failed transaction fire it too. |

See the [transaction lifecycle](/payments/core-concepts/transaction-lifecycle) for the statuses this event reports and the order they occur in.

## Cash-in

| Event              | Fires when                                                                                   |
| ------------------ | -------------------------------------------------------------------------------------------- |
| `cashIn.created`   | A cash-in code was issued.                                                                   |
| `cashIn.completed` | The cash was deposited and converted; the auto-created transaction is linked on the cash-in. |
| `cashIn.expired`   | The code expired before a deposit was made.                                                  |

## Virtual accounts

Lifecycle events report the account itself. The `deposit`, `cryptoTransfer`, and `fiatTransfer` events report the individual legs of money moving through it.

| Event                                     | Fires when                                                                  |
| ----------------------------------------- | --------------------------------------------------------------------------- |
| `virtualAccount.provisioned`              | The underlying deposit account was provisioned; `bankDetails` is populated. |
| `virtualAccount.active`                   | The account is live and accepting deposits.                                 |
| `virtualAccount.frozen`                   | Deposits are suspended.                                                     |
| `virtualAccount.closed`                   | The account reached end of life.                                            |
| `virtualAccount.deleted`                  | An asynchronous delete finalized.                                           |
| `virtualAccount.failed`                   | Provisioning failed.                                                        |
| `virtualAccount.statusChanged`            | The account's status changed.                                               |
| `virtualAccount.deposit.pending`          | An inbound bank deposit was detected and is settling.                       |
| `virtualAccount.deposit.settled`          | The inbound deposit settled.                                                |
| `virtualAccount.deposit.failed`           | The inbound deposit failed.                                                 |
| `virtualAccount.deposit.returned`         | The inbound deposit was returned by the bank.                               |
| `virtualAccount.cryptoTransfer.initiated` | The crypto leg was initiated toward the destination wallet.                 |
| `virtualAccount.cryptoTransfer.pending`   | The crypto leg is in flight.                                                |
| `virtualAccount.cryptoTransfer.settled`   | The crypto leg was delivered.                                               |
| `virtualAccount.cryptoTransfer.failed`    | The crypto leg failed.                                                      |
| `virtualAccount.fiatTransfer.initiated`   | The fiat leg was initiated toward the destination account.                  |
| `virtualAccount.fiatTransfer.pending`     | The fiat leg is in flight.                                                  |
| `virtualAccount.fiatTransfer.settled`     | The fiat leg was delivered.                                                 |
| `virtualAccount.fiatTransfer.failed`      | The fiat leg failed.                                                        |

## Deposit addresses

Lifecycle events use the `depositAddress.` prefix. Per-deposit and per-payout legs use the `deposit_address.` prefix.

| Event                                                                                            | Fires when                                                                                                                           |
| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `depositAddress.active`                                                                          | The inlet address is provisioned and monitoring; `depositInstructions` is populated.                                                 |
| `depositAddress.frozen`                                                                          | Deposits are held pending review.                                                                                                    |
| `depositAddress.closed`                                                                          | The address reached end of life.                                                                                                     |
| `depositAddress.failed`                                                                          | Provisioning failed.                                                                                                                 |
| `depositAddress.statusChanged`                                                                   | The address's status changed.                                                                                                        |
| `deposit_address.crypto_deposit.pending`                                                         | An inbound onchain deposit was detected.                                                                                             |
| `deposit_address.crypto_deposit.settled`                                                         | The inbound deposit settled.                                                                                                         |
| `deposit_address.crypto_deposit.failed`                                                          | The inbound deposit failed.                                                                                                          |
| `deposit_address.crypto_deposit.needs_attribution`                                               | The deposit came from an unrecognized sender and is held for [sender attribution](/payments/guides/deposit-addresses#held-deposits). |
| `deposit_address.ach_payout.initiated` / `.pending` / `.settled` / `.failed` / `.returned`       | The ACH payout leg progressed.                                                                                                       |
| `deposit_address.wire_payout.initiated` / `.pending` / `.settled` / `.failed` / `.returned`      | The domestic wire payout leg progressed.                                                                                             |
| `deposit_address.intl_wire_payout.initiated` / `.pending` / `.settled` / `.failed` / `.returned` | The international wire payout leg progressed.                                                                                        |
| `deposit_address.crypto_payout.initiated` / `.pending` / `.settled` / `.failed`                  | The crypto payout leg progressed.                                                                                                    |

The fiat payout legs report a `returned` state because a bank can reverse a settled transfer. The crypto payout leg has no `returned` event.

## External accounts

| Event                           | Fires when                                                                       |
| ------------------------------- | -------------------------------------------------------------------------------- |
| `externalAccount.created`       | The account was registered and is being validated.                               |
| `externalAccount.verified`      | Validation passed; the account is usable on quotes.                              |
| `externalAccount.declined`      | Validation failed.                                                               |
| `externalAccount.deleted`       | The account was removed.                                                         |
| `externalAccount.statusChanged` | The account's status changed. A transition to `invalid` always fires this event. |

## Counterparties

| Event                        | Fires when                       |
| ---------------------------- | -------------------------------- |
| `counterparty.statusChanged` | A counterparty's status changed. |

## Endorsements

| Event                 | Fires when                                                                  |
| --------------------- | --------------------------------------------------------------------------- |
| `endorsement.updated` | A customer endorsement changed state.                                       |
| `endorsement.active`  | An endorsement became active; the customer can use the operations it gates. |

## Wallets

| Event                | Fires when                                            |
| -------------------- | ----------------------------------------------------- |
| `wallet.provisioned` | A custodial wallet's onchain address was provisioned. |

## Compliance review events

These events fire when a transaction is held for compliance review. They are excluded from the `*` wildcard and must be named explicitly in `subscriptions`.

| Event                                    | Fires when                                     |
| ---------------------------------------- | ---------------------------------------------- |
| `transaction.fiatToCrypto.underReview`   | A fiat-funded transaction was held for review. |
| `transaction.cryptoToFiat.underReview`   | A payout was held for review.                  |
| `transaction.cryptoToCrypto.underReview` | A crypto send was held for review.             |

## Delivery and verification

* Delivery is at-least-once, and ordering is best effort rather than a guarantee. Treat deliveries as unordered and possibly duplicated: deduplicate on `id`, and apply an event only when its `sequence` is newer than the last one you processed for that resource, so a late delivery cannot overwrite newer data.
* Signature verification: the `Webhook-Signature` header is `t=<unix>,v1=<hex>`, where the hex is an HMAC-SHA256 over `<t>.<raw request body>` keyed with your endpoint's signing key. The timestamp and signature are regenerated on every attempt, so verify against the raw body before parsing it, and reject stale timestamps.
* Manage subscriptions with the [Webhooks endpoints](/api-reference/overview): `POST`/`GET /webhooks`, `GET`/`PATCH`/`DELETE /webhooks/{webhookId}`, plus lifecycle actions on `POST /webhooks/{webhookId}/enable`, `POST /webhooks/{webhookId}/disable`, `POST /webhooks/{webhookId}/rotate-key`, and `POST /webhooks/{webhookId}/test`.
* Inspect delivery history with `GET /webhooks/{webhookId}/deliveries` (filter by `status`, `eventId`, `test`, or a `createdAfter`/`createdBefore` window). Expand a single delivery with its HTTP attempts with `GET /webhooks/{webhookId}/deliveries/{deliveryId}`. Requeue failed deliveries with `POST /webhooks/{webhookId}/deliveries/retry`: pass explicit `deliveryIds`, or a `status` plus a `createdAfter`/`createdBefore` range to retry all matching deliveries.
* A webhook is considered down after continuous failure: no successful delivery, and at least one failed attempt since the last success. Two separate thresholds apply. `notifyThresholdSecs` controls **notification** only (range 10 minutes to 1 day, default 1 hour), and fires once per down episode rather than per failed attempt. **Auto-suspension** is separate and happens after roughly 3 days of continuous failure.
* A suspended webhook keeps creating delivery rows for matching events, but records them as `skipped` rather than `queued`, so nothing is silently lost. Only the service sets `suspended`, and it never clears it: return the webhook to `enabled` with `POST /webhooks/{webhookId}/enable`, which resets the down clock, then re-fire the skipped deliveries with `POST /webhooks/{webhookId}/deliveries/retry`. A `disabled` webhook differs: it creates no delivery rows at all.
* Any non-2xx response, timeout, or network error is retried on a fixed schedule. Individual status codes are not special-cased: a `410 Gone` is treated the same as a `500`.

| Attempts so far       | 0  | 1  | 2   | 3   | 4  | 5   | 6  | 7  | 8  | 9  |
| --------------------- | -- | -- | --- | --- | -- | --- | -- | -- | -- | -- |
| Delay before the next | 0s | 5s | 10s | 30s | 1m | 10m | 1h | 6h | 1d | 2d |

That is 10 attempts over roughly 3 days, after which the delivery is marked `failed`. Each attempt is bounded by the webhook's `timeoutMs` (default 5000, hard cap 10000), measured from connect through response. A manual retry overrides the cap: it resets the attempt count and requeues the delivery immediately.

* `DELETE /webhooks/{webhookId}` is a soft delete: the webhook is hidden from list and get, but delivery history is never removed with it. History is retained for the configured retention window (30 days by default), so existing `whd_` links keep resolving.
