source instrument to a destination instrument. Its top-level status is built for branching your code. subStatus is always present and adds operational detail for display and logging. Each movement of the money on a rail is a separate transfer with its own status.
Anatomy of a transaction
precursornames what created the transaction:quote,depositAddress,virtualAccount,cashIn, orwalletDeposit(a crypto deposit straight into a custodial wallet, with noid).sourceanddestinationare instruments. Bank instruments carry no rail; the rail is on the transfer.failureappears only whenstatusisfailed, andholdonly whenstatusisawaitingAction.
Quotes report the corridor as
sourceToDestination, and GET /transactions?sourceToDestination=... filters on it.
Status model
Branch on
status. Use subStatus for display, logging, and support. A payout in flight never changes status: a withdrawal reads processing from creation until the money lands.
completed and failed are final for almost every transaction. The exceptions are covered in Failed transactions and returns.
Holds
Whilestatus is awaitingAction, the hold object carries a code and a fixed message:
Sub-statuses
subStatus is a dotted string whose prefix is always the status. processing, completed, and failed each have a default (processing.initiated, completed.settled, failed.unspecified) used when nothing more specific applies. Every awaitingAction value names a specific blocking reason.
processing
awaitingAction
completed
failed
failure.code gives the machine-readable reason (for example insufficientFunds, payoutFailed, or accountClosed) and failure.retryable tells you whether trying again can succeed. See ReasonCode in the API reference for every value.
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,instant,intraBank,blockchain,card, orcash.instantis an RTP or FedNow payout, and the transfer names which one ininstantRail.intraBankstays inside one bank, and the transfer names the same-bank rail inintraBankRail.attemptis1for the first send. A payout sent again after a confirmed failure is a new transfer with the nextattempt, and the failed one staysfailed. A re-send of a payout whose outcome is unknown stays the same transfer.
GET /transfers/{transferId} returns the full transfer: status, amount, the from and to instruments, the ACH options it was sent with (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 has one.
Failed transactions and returns
completed and failed can still move afterwards in a few cases:
- A bank takes back money that settled. A returned payout or refund reads
processing.underReviewwhile OMS decides whether to resend it or send it back. A returned deposit readsfailed.returnedToSender. - A failed deposit settles late. On a late provider report, the transaction returns to
processingand continues tocompleted. - A refund starts after a failure. While it is in flight the transaction reads
processing.returningToSender; once it settles,failed.returnedToSender.
refund transfer on the transaction.
Where returned funds go
Deposit addresses and virtual accounts accept an optional
returnDestination at create or PATCH; send null in a PATCH to clear it.
- For a deposit address, it is a crypto target: an OMS wallet (
walletCrypto, which must be multi-asset, otherwise422 ReturnDestinationMustBeMultiAsset) or a registered external wallet (walletExternal), plus anetworkthat matches the network the address accepts. - For a virtual account, it is a registered bank account (
bankUs,bankIban, orbankCanada) with a required rail innetwork. Only USD bank rails are accepted today:walletFiat, andbankCanadawithnetwork: "local", are rejected with422 RailNotSupported.
Webhook events
Each transaction and transfer change is delivered as a webhook, so you rarely need to pollGET /transactions/{transactionId}.
The event’s
data.resource is the transaction or transfer as GET returns it, so branch on data.resource.status. On statusChanged events, data.previousStatus names the status it left. A sub-status-only change still sends transaction.statusChanged, with previousStatus equal to status. Deliveries are at least once and can arrive out of order: deduplicate on the event id and order by createdAt per resource. See Receiving webhooks and the webhook events catalog.
Idempotency
POST requests that create a quote, transaction, wallet, or other money-movement resource require an Idempotency-Key header; the API reference marks it on each operation. Use one UUID per logical operation, tied to your internal order ID, and reuse it only to retry that same request. OMS returns the original response for a repeat within 24 hours. Reusing a key with a different body returns 422 IdempotencyKeyReuse.