The money lifecycle
Every state a deposit, withdrawal, conversion or payout passes through, which webhook fires when, and what you should do at each one.
Intents
Every call that moves money creates an intent: a durable record of what you
asked for, what happened, and how far it got. POST /deposits,
POST /withdrawals, POST /conversions and POST /payouts each create one. Your
Idempotency-Key claims exactly one intent, forever. Read it back at any time
with GET /deposits/{id}, GET /withdrawals/{id}, GET /conversions/{id} or
GET /payouts/{id}.
The six states are named for what happened to your money, not for a
pipeline stage. The same six apply to all four flows. A payout's states are
named the same way, but read PROCESSING rather than EXECUTING_ONCHAIN: a
payout runs on a fiat rail, not a chain, so a state name that implied one
would be a lie. See Payouts below for its own ladder, including
the RETURNED case that has no counterpart on the other three flows.
| State | Money |
|---|---|
PENDING | Nothing has moved. Pre-flight passed and the key is claimed. |
EXECUTING_ONCHAIN | In flight. |
SETTLED | Done. It went where you asked. |
FAILED | Nothing moved, and that is positively established, not assumed. |
FAILED_PENDING_COMPENSATION | Money moved and the unwind is not confirmed. TCB is resolving it. |
COMPENSATED | TCB resolved it and you have been made whole. |
Three of them are terminal: SETTLED, FAILED and COMPENSATED.
Each ladder below reads top to bottom, and every arrow is coloured by where it lands: blue for in flight, green for settled, red for a failure where nothing moved, amber for a compensable one. Compare the four and the differences are the whole point of this page.
Deposits
| State | What it means | What to do | Webhook |
|---|---|---|---|
PENDING | Your fiat has not been debited. | Poll, or wait for the webhook. | — |
EXECUTING_ONCHAIN | The mint and the vault deposit are in flight. | Wait. Do not resubmit. | — |
SETTLED | Your shares are minted and your position reflects them. | Nothing. | deposit.settled |
FAILED | Nothing was charged. | Fix whatever the error names and submit a new request with a new key. | deposit.failed |
FAILED_PENDING_COMPENSATION | Your fiat may have been taken. | See Compensation below. | none |
COMPENSATED | TCB closed it out. | Reconcile against the record. | none |
Withdrawals
A withdrawal walks the same six states, with one difference.
A withdrawal that has started executing can never be declared FAILED.
Every failure after that point lands FAILED_PENDING_COMPENSATION. Only a
PENDING withdrawal, where nothing was ever attempted, reaches FAILED.
Build for this. An integration that treats "not settled and not pending" as "failed, so retry" will withdraw twice.
| State | What it means | What to do | Webhook |
|---|---|---|---|
PENDING | Nothing has left the vault. | Poll, or wait for the webhook. | — |
EXECUTING_ONCHAIN | The vault withdraw, burn and fiat credit are in flight. | Wait. | — |
SETTLED | The fiat is on your account and the reserve is released. | Nothing. | withdrawal.settled |
FAILED | Nothing was attempted. Reachable only from PENDING. | Submit a new request with a new key. | withdrawal.failed |
FAILED_PENDING_COMPENSATION | Shares left the vault; the rest is unconfirmed. | See Compensation. | none |
COMPENSATED | You have been paid what you were owed. | Reconcile. | none |
Conversions
A conversion walks the same six states, and reaches them the way a deposit
does rather than the way a withdrawal does. The order record is written before
anything executes, so its absence proves nothing moved. A conversion can
therefore reach a terminal FAILED from EXECUTING_ONCHAIN.
| State | What it means | What to do | Webhook |
|---|---|---|---|
PENDING | Nothing has been sold. | Poll, or wait. | — |
EXECUTING_ONCHAIN | The order is live. | Wait. | — |
SETTLED | Both account legs moved. | Nothing. | conversion.settled |
FAILED | Nothing moved. | New request, new key. | conversion.failed |
FAILED_PENDING_COMPENSATION | The order filled — in whole or in part — and the outcome could not be recorded. | See Compensation. | none |
COMPENSATED | Resolved by an operator. | Reconcile. | none |
Payouts
A payout's states carry the same meaning as the other three flows, presented
under the wire names PROCESSING (in place of EXECUTING_ONCHAIN) and, on
top, RETURNED — a state none of the other flows has, because none of them
can be sent back by a receiving institution after settling.
| State | What it means | What to do | Webhook |
|---|---|---|---|
PENDING | Nothing has been debited. | Poll, or wait for the webhook. | — |
HELD_FOR_REVIEW | Your account is debited and TCB is reviewing the payment before it goes out. Nothing has been sent. | Wait. Released, it moves to PROCESSING; declined, it ends FAILED with the debit refunded. | payout.held |
PROCESSING | Your account is debited and the rail has the payment. | Wait. | — (payout.released when it leaves HELD_FOR_REVIEW) |
SETTLED | The rail confirmed the payment. | Nothing, unless the receiving bank later returns it. | payout.settled |
RETURNED | A payout the receiving bank sent back — usually after it settled, sometimes while it was still PROCESSING. The amount is re-credited to the source account. | Reconcile against the record. | payout.returned |
FAILED | Nothing left your account. A taken debit was refunded. | Fix whatever the error names and submit a new request with a new key. | payout.failed |
FAILED_PENDING_COMPENSATION | Money may have left. | See Compensation below. | none |
COMPENSATED | TCB resolved it. | Read compensation.outcome and reconcile. | none |
A payout that reached the rail is declared FAILED only on the rail's own
answer that it did not take the payment, never on inference. Anything short
of that positive answer — a thrown submission, an unconfirmed acceptance — is
FAILED_PENDING_COMPENSATION, not FAILED.
Which webhook fires when
Only terminal transitions are evented — two per flow, plus three for
payouts: payout.returned, explained below, and the review pair payout.held
and payout.released:
| Event | Fires when |
|---|---|
deposit.settled | A deposit reached SETTLED |
deposit.failed | A deposit reached terminal FAILED — nothing moved |
withdrawal.settled | A withdrawal reached SETTLED |
withdrawal.failed | A withdrawal reached terminal FAILED — nothing moved |
conversion.settled | A conversion reached SETTLED |
conversion.failed | A conversion reached terminal FAILED — nothing moved |
payout.settled | A payout reached SETTLED |
payout.failed | A payout reached terminal FAILED — nothing left your account |
payout.returned | A payout was sent back and re-credited — after settlement, or while still PROCESSING |
payout.held | A payout was debited and held for review by TCB; nothing has been sent |
payout.released | TCB released a held payout; it moved to PROCESSING |
transfer.completed | A within-tree transfer committed |
token_mint.settled | A tcbUSD mint reached SETTLED |
token_mint.failed | A tcbUSD mint reached terminal FAILED — nothing was charged |
token_redemption.settled | A tcbUSD redemption reached SETTLED |
token_redemption.failed | A tcbUSD redemption reached terminal FAILED — nothing was burned |
share_subscription.settled | A vault subscription reached SETTLED |
share_subscription.failed | A vault subscription reached terminal FAILED — nothing went into the vault |
share_redemption.settled | An instant vault redemption reached SETTLED |
share_redemption.failed | An instant vault redemption reached terminal FAILED — nothing left the vault |
share_redemption_request.queued | A queued vault redemption is waiting in the vault queue |
share_redemption_request.settled | A queued vault redemption is final and something was paid |
share_redemption_request.cancelled | A queued vault redemption was cancelled before anything was paid |
share_redemption_request.failed | A queued vault redemption never reached the vault queue — nothing moved |
No event is sent for FAILED_PENDING_COMPENSATION or COMPENSATED. TCB
resolves both states — automatically where the outcome can be proven, by an
operator otherwise. Watch for them on the read endpoints instead.
payout.returned is the one event on this table that is not a terminal
transition: it fires on an already-SETTLED payout moving to RETURNED, not
on a state reaching its terminal one. A return can also arrive on a payout
that is still PROCESSING; then payout.settled and payout.returned fire
together, in that order, and the read shows RETURNED.
There is no transfer.failed. A within-tree transfer either commits, and the
response says where the money is, or it never happened and you hold a 4xx.
No third outcome exists, so do not wait for an event.
Event ordering is not guaranteed and neither is single delivery. Dedupe on the
tcb-webhook-id header, and treat the read endpoint as the truth.
Settled, reserved, available
For each of your vault sub-accounts:
settledShares— what the sub-account holds, the sum of its posted entries. A settled deposit adds; a settled withdrawal subtracts.reservedShares— shares held back because a withdrawal from this sub-account is in flight. Reserved at the moment the withdrawal is authorised, in the same transaction that claims the intent.availableShares—settledSharesminusreservedShares. This is what a new withdrawal is checked against, and a request for more than this is refused with400INSUFFICIENT_SUB_ACCOUNT_POSITIONbefore anything moves.
A reserve is released only by the settlement that also posts the withdrawal, in the same transaction.
A stuck withdrawal keeps its reserve
If a withdrawal ends FAILED_PENDING_COMPENSATION, its reserve stays held
until TCB resolves the intent. The shares may already have left the
vault, and releasing the availability would let the same shares be spent twice.
Your availableShares stays reduced by that amount. GET /positions shows
which sub-account is affected and by how much.
Compensation
*_FAILED_PENDING_COMPENSATION means money moved on one leg and the other
did not confirm. It states a real position. It is not a generic code for an
unknown outcome.
What happens next:
- TCB resolves it from the evidence. The record carries what the request path established, including the transaction receipt where one exists. Where that evidence proves the outcome, TCB completes the resolution automatically; otherwise an operator works it by hand. Nothing resolves one merely because time passed.
- You will be told. Contact partners@threecomma.group if you are holding one and have not heard.
- The intent stays readable. Poll
GET /deposits/{id},GET /withdrawals/{id},GET /conversions/{id}orGET /payouts/{id}; the state moves toCOMPENSATEDwhen it is closed out.
For a payout, TCB resolves the compensable state from the rail's own answer about the payment: refunding where the rail holds no such payment or reports that it failed, completing forward where the rail holds it. Any other answer — including a payment the rail reports as already returned — is worked by an operator rather than resolved automatically.
Never resubmit with a new key
A fresh idempotency key is a second, independent money movement. Retrying a compensable deposit deposits again; retrying a compensable withdrawal withdraws again. Poll the original key. Always.
On a withdrawal, COMPENSATED means the fiat you were owed reached you. It
does not mean the withdrawal was reversed. The shares are already gone, so the
resolution completes forward.
Three related codes mean something different. Do not confuse them with a
compensable failure. DEPOSIT_SETTLEMENT_NOT_RECORDED,
WITHDRAWAL_SETTLEMENT_NOT_RECORDED and CONVERSION_SETTLEMENT_NOT_RECORDED
mean the money completed and only the bookkeeping is missing. Nothing is
owed and nothing is in flight.
LEDGER_ENTRY_NOT_RECORDED also means the money completed, and it is the one
code in this group that leaves something outstanding. The exact share movement
could not be read back, so no ledger entry was posted and the intent stays
EXECUTING_ONCHAIN instead of reaching a terminal state. On a withdrawal the
reserve is still held, so that sub-account's availableShares stays reduced
until an operator posts the entry. Contact TCB and quote the id.
All four codes carry the same instruction: poll the original key, and never resubmit under a new one.
Rail freeze
PARTNER_RAIL_FROZEN (409) means TCB's reconciliation found a share
divergence on your position in a vault that it cannot explain, and has stopped
the rail rather than move more money across it.
| Scope | Your principal, that vault. Deposits into it and withdrawals out of it are both refused, and so is every payout from any of your accounts while a freeze is open on your principal — money does not leave on your behalf until a person has looked. Other currencies' vault legs and other clients are unaffected. |
| What was charged | Nothing. The refusal happens before anything moves, and the message says so. |
| Your position | Unchanged and still readable. GET /positions works throughout. |
| How it lifts | A person at TCB establishes what happened and lifts it. Nothing lifts one on a timer. |
| What you should do | Stop retrying and contact TCB. A retry cannot succeed. |
Idempotency across retries
On any timeout, dropped connection or 5xx, retry with the same
Idempotency-Key. A new key on a retry path is a second money movement.
What each combination of key and parameters answers, how long a key is remembered, and which characters it may contain are in Conventions.
How it works
The object model: sub-clients, accounts, vault sub-accounts and positions.
Glossary
Every term these pages use, defined once.
Errors
Every code, with a retry-safety column.
Webhooks
Register an endpoint, verify signatures, handle retries.
Go live
The checklist before you point real volume at this.