status is designed for programmatic branching. subStatus is always present and provides operational detail without complicating your core logic.
Direction
A transaction’s direction is the pair of itssource and destination instrument categories. OMS infers it from the two instruments; you do not set it. Quotes report it as sourceToDestination, and GET /transactions accepts the same values as a sourceToDestination filter:
Transfers
A transaction records what moved from where to where. Each movement of its money on one rail is a transfer (trf_ prefix). The transaction lists its transfers in creation order in transfers, each as { id, role, rail, attempt }; the array is left out until the first transfer exists.
roleisdeposit(money coming in),payout(money going out to the destination), orrefund(money sent back to the sender).railis how the money moved:ach,achSameDay,wire,internationalWire,rtp,bookTransfer,blockchain,card, orcash. The transaction’ssourceanddestinationinstruments carry no rail.attemptis1for the first send. A payout sent again after a confirmed failure is a new transfer with the nextattempt; the failed one staysfailed.
GET /transfers/{transferId} for its status, amount, from and to instruments, ACH options (secCode, companyDiscretionaryData), originatorName, and tracking. tracking holds the references that trace the transfer outside OMS (txHash, imad, uetr, endToEndId, instructionId) and is left out until OMS holds one.
An outgoing transfer (payout or refund) moves through initiated, pending, and settled; a deposit moves through pending and settled. Any transfer can end failed, with a failure object. A payout the receiving bank sends back, or a virtual account deposit the sending bank takes back, ends returned after settled, with a return object.
Status model
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 what you must do, and the transaction returns to processing once cleared.
The hold object carries a code and a fixed message for that code:
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). processing, completed, and failed each carry a default member (processing.initiated, completed.settled, and failed.unspecified) when no more specific value applies; every awaitingAction value names a specific blocking reason. Cash off-ramp lifecycle events surface here rather than as top-level statuses, and payout execution does too.
Payout execution reports its stages on subStatus:
Terminal failures that match these stages also carry a dedicated reason:
Use
subStatus for display and logging, not for control flow. A payout in flight never changes the top-level status: a withdrawal reads processing.fundsPulled until its payout transfer exists, then processing.cryptoOut or processing.fiatOut, and processing.payoutUnconfirmed or processing.payoutRetrying on the exceptional branches.
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 payout cannot be completed, the funds are returned in the same asset that was received, with no action required from you. The return is a refund transfer on the transaction. While it is in flight, the transaction reads processing.returningToSender; once it settles, the transaction reads failed with subStatus failed.returnedToSender.
Each status change 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:Configuring return destinations
Deposit addresses and virtual accounts accept an optionalreturnDestination 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 network the address accepts (expectedSourceNetwork on create, read back as depositInstructions.network). 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.
Webhook events
OMS fires a webhook on every meaningful state change. Each delivery carries the full transaction object underdata, 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:
["*"] 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 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 toprocessing. The same status model and webhook events apply.
Idempotency
EveryPOST endpoint except the token endpoints (POST /auth/token and POST /customers/{customerId}/token) requires 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.