> ## 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.

# Transaction lifecycle

> How a transaction moves through OMS from creation to completion, including statuses, sub-statuses, and webhook events.

Every transaction in OMS moves through a predictable set of statuses. The top-level `status` is designed for programmatic branching. `subStatus` is always present and provides operational detail without complicating your core logic.

## Direction

Every transaction carries a `sourceToDestination` field: a composite of the source and destination instrument categories.

| Value                 | What it does                                                     |
| --------------------- | ---------------------------------------------------------------- |
| `cryptoToCrypto`      | USDC moves from one wallet to another, or to an external address |
| `cryptoToFiatAccount` | USDC out of a wallet, fiat delivered to a bank account           |
| `cryptoToCash`        | USDC out of a wallet, delivered as a cash pickup                 |
| `fiatAccountToCrypto` | Fiat in from a bank account, USDC delivered to a wallet          |
| `cashToCrypto`        | Cash in from a retail deposit, USDC delivered to a wallet        |

OMS infers the direction from the source and destination instruments. You do not set it explicitly.

## Status model

```
processing                      (initial state; the quote is already accepted)
    │
    ├──► completed              (standard flows)
    │
    ├──► awaitingAction         (non-terminal; blocked on developer, upstream, or compliance action)
    │        └──► processing    (once the hold clears)
    │
    └──► failed
```

The four top-level statuses are `processing`, `awaitingAction`, `completed`, and `failed`. A transaction begins at `processing` the moment it is created from a quote. `awaitingAction` is a non-terminal hold state; the `hold` object explains the reason and carries a deadline, and the transaction returns to `processing` once cleared.

The `hold` object is discriminated by its `type` field:

| `hold.type`              | Meaning                                                                                                                                                                                               |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `senderAttribution`      | A deposit address received crypto from a sender not linked to a counterparty. Carries `matchableExternalAccountCriteria`; registering a matching `walletExternal` external account releases the hold. |
| `depositAddressFrozen`   | The deposit address is frozen.                                                                                                                                                                        |
| `depositAddressInactive` | The deposit address's destination external account became unusable. The `cause` object identifies the external account, its status, and the reason it became invalid.                                 |

Each hold carries `since`, an optional `deadline`, and `resolvedAt` once cleared.

**Branch on `status` only.** `subStatus` is always present. It is a status-scoped string carrying operational detail namespaced by its parent status (for example `processing.fundsPulled`, `processing.cashPickupReady`, or `completed.cashPickupCollected`). Each status carries a default member when no more specific value applies, so you always see a concrete sub-state on every transaction. Cash off-ramp lifecycle events surface here rather than as top-level statuses, and payout execution does too: `processing.awaitingCryptoOut`, `processing.cryptoOut`, `processing.awaitingFiatOut`, and `processing.fiatOut` track the crypto and fiat out-legs of a payout. Use `subStatus` for display and logging, not for control flow.

The enum is open: new members may be added without an API version bump. If your client receives an unrecognized value, fall back to the status prefix before the dot (an unrecognized `processing.someFutureThing` still means `processing`).

## Failed transactions and returns

`failed` is terminal for the transaction, but not for the funds. When OMS has already received the inbound funds and the outbound leg cannot be completed, the funds are returned. Returns are executed by OMS operations, so no action is required from you, and are always made in the same asset that was received. The transaction's `subStatus` tracks progress:

| `subStatus`             | Meaning                                                               |
| ----------------------- | --------------------------------------------------------------------- |
| `failed.returnPending`  | The return is scheduled but has not started.                          |
| `failed.returnStarted`  | The return is in progress.                                            |
| `failed.returnComplete` | The funds were delivered to the return destination.                   |
| `failed.returnFailed`   | The return itself could not be completed; see `error.manualRecovery`. |

Each step fires `transaction.statusChanged`, so you can render a "refund in progress" state without polling.

### Where returned funds go

The return path depends on how the transaction was created:

| Created by      | Return path                                                                                       |
| --------------- | ------------------------------------------------------------------------------------------------- |
| Quote           | Back to the source payment method: the wallet or card that funded it. Nothing to configure.       |
| Deposit address | The deposit address's `returnDestination`, or the on-chain sender when none is set.               |
| Virtual account | The virtual account's `returnDestination`, or held for manual refund processing when none is set. |

### Configuring return destinations

Deposit addresses and virtual accounts accept an optional `returnDestination` at create or `PATCH`. Send an explicit `null` in a `PATCH` body to clear one you set earlier.

For a deposit address, `returnDestination` is a crypto target: a multi-asset OMS wallet (`walletCrypto`) or a registered external wallet (`walletExternal`), plus the `network` to return on, which must match the address's `expectedSourceNetwork`. A custodial, non-multi-asset OMS wallet is rejected with `422 returnDestinationMustBeMultiAsset`.

For a virtual account, it is a fiat target: a registered bank account (`bankUs`, `bankIban`, or `bankCanada`), each with a required rail in `network`. USD bank rails are the only ones accepted today; the fiat balance wallet (`walletFiat`) and CAD (`bankCanada` with `network: "local"`) are rejected with `422 railNotSupported`.

<Tip>
  Set a `returnDestination` on every deposit address and virtual account. Without one, returned crypto goes back to the on-chain sender, which may be an exchange hot wallet the customer cannot receive on, and returned fiat is held for manual processing.
</Tip>

## Webhook events

OMS fires a webhook on every meaningful state change. Each delivery carries the full transaction object under `data`, so your handler reads the `status` (and, if needed, `subStatus`) directly off it. Polling `GET /transactions/{transactionId}` is rarely necessary once webhooks are configured.

Subscribe to events through the Webhooks endpoints or the OMS Dashboard. A subscription is an endpoint URL plus the list of event types you want:

```
POST /webhooks
{
  "url": "https://your-app.example.com/oms/webhooks",
  "subscriptions": ["*"]
}
```

Pass `["*"]` in `subscriptions` to receive every event type, or list specific event names to filter. The Webhooks endpoints support full CRUD (`GET`/`PATCH`/`DELETE /webhooks/{webhookId}`) plus lifecycle actions (`POST /webhooks/{webhookId}/enable | /disable | /rotate-key | /test`), and the create response returns a `signingKey` once, which you use to verify the `Webhook-Signature` header on incoming deliveries. Inspect and replay delivery history at `GET /webhooks/{webhookId}/deliveries` and `POST /webhooks/{webhookId}/deliveries/retry`.

Transactions emit one event, `transaction.statusChanged`, on every status transition described above, in either direction and on any rail. The full transaction arrives under `data`, so branch on `data.status` and use `data.subStatus` for detail. The envelope's `previousStatus` names the status it left. To be notified when a transaction is held for compliance review, subscribe explicitly to `transaction.fiatToCrypto.underReview`, `transaction.cryptoToFiat.underReview`, or `transaction.cryptoToCrypto.underReview`; the `*` wildcard does not include them. See the [webhook events catalog](/api-reference/webhook-events) for every event type and the delivery envelope.

## Auto-created transactions

Deposit addresses, virtual accounts, and cash-ins bypass the quote step. OMS creates the transaction internally when funds arrive and moves it directly to `processing`. The same status model and webhook events apply.

## Idempotency

All `POST` endpoints accept an `Idempotency-Key` header. Use a stable key (UUID tied to your internal order ID) to safely retry on network failure. OMS returns the same response for any subsequent request with the same key within the idempotency window.

<Tip>
  Set `Idempotency-Key` on every write request in production. It protects against double-execution without any coordination on your side.
</Tip>
