Pilot environment: testnet, simulated yield, mocked fiat rails — read the disclosure
Concepts

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.

StateMoney
PENDINGNothing has moved. Pre-flight passed and the key is claimed.
EXECUTING_ONCHAINIn flight.
SETTLEDDone. It went where you asked.
FAILEDNothing moved, and that is positively established, not assumed.
FAILED_PENDING_COMPENSATIONMoney moved and the unwind is not confirmed. TCB is resolving it.
COMPENSATEDTCB 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

StateWhat it meansWhat to doWebhook
PENDINGYour fiat has not been debited.Poll, or wait for the webhook.—
EXECUTING_ONCHAINThe mint and the vault deposit are in flight.Wait. Do not resubmit.—
SETTLEDYour shares are minted and your position reflects them.Nothing.deposit.settled
FAILEDNothing was charged.Fix whatever the error names and submit a new request with a new key.deposit.failed
FAILED_PENDING_COMPENSATIONYour fiat may have been taken.See Compensation below.none
COMPENSATEDTCB 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.

StateWhat it meansWhat to doWebhook
PENDINGNothing has left the vault.Poll, or wait for the webhook.—
EXECUTING_ONCHAINThe vault withdraw, burn and fiat credit are in flight.Wait.—
SETTLEDThe fiat is on your account and the reserve is released.Nothing.withdrawal.settled
FAILEDNothing was attempted. Reachable only from PENDING.Submit a new request with a new key.withdrawal.failed
FAILED_PENDING_COMPENSATIONShares left the vault; the rest is unconfirmed.See Compensation.none
COMPENSATEDYou 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.

StateWhat it meansWhat to doWebhook
PENDINGNothing has been sold.Poll, or wait.—
EXECUTING_ONCHAINThe order is live.Wait.—
SETTLEDBoth account legs moved.Nothing.conversion.settled
FAILEDNothing moved.New request, new key.conversion.failed
FAILED_PENDING_COMPENSATIONThe order filled — in whole or in part — and the outcome could not be recorded.See Compensation.none
COMPENSATEDResolved 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.

StateWhat it meansWhat to doWebhook
PENDINGNothing has been debited.Poll, or wait for the webhook.—
HELD_FOR_REVIEWYour 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
PROCESSINGYour account is debited and the rail has the payment.Wait.— (payout.released when it leaves HELD_FOR_REVIEW)
SETTLEDThe rail confirmed the payment.Nothing, unless the receiving bank later returns it.payout.settled
RETURNEDA 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
FAILEDNothing 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_COMPENSATIONMoney may have left.See Compensation below.none
COMPENSATEDTCB 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:

EventFires when
deposit.settledA deposit reached SETTLED
deposit.failedA deposit reached terminal FAILED — nothing moved
withdrawal.settledA withdrawal reached SETTLED
withdrawal.failedA withdrawal reached terminal FAILED — nothing moved
conversion.settledA conversion reached SETTLED
conversion.failedA conversion reached terminal FAILED — nothing moved
payout.settledA payout reached SETTLED
payout.failedA payout reached terminal FAILED — nothing left your account
payout.returnedA payout was sent back and re-credited — after settlement, or while still PROCESSING
payout.heldA payout was debited and held for review by TCB; nothing has been sent
payout.releasedTCB released a held payout; it moved to PROCESSING
transfer.completedA within-tree transfer committed
token_mint.settledA tcbUSD mint reached SETTLED
token_mint.failedA tcbUSD mint reached terminal FAILED — nothing was charged
token_redemption.settledA tcbUSD redemption reached SETTLED
token_redemption.failedA tcbUSD redemption reached terminal FAILED — nothing was burned
share_subscription.settledA vault subscription reached SETTLED
share_subscription.failedA vault subscription reached terminal FAILED — nothing went into the vault
share_redemption.settledAn instant vault redemption reached SETTLED
share_redemption.failedAn instant vault redemption reached terminal FAILED — nothing left the vault
share_redemption_request.queuedA queued vault redemption is waiting in the vault queue
share_redemption_request.settledA queued vault redemption is final and something was paid
share_redemption_request.cancelledA queued vault redemption was cancelled before anything was paid
share_redemption_request.failedA 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 — settledShares minus reservedShares. This is what a new withdrawal is checked against, and a request for more than this is refused with 400 INSUFFICIENT_SUB_ACCOUNT_POSITION before 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:

  1. 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.
  2. You will be told. Contact partners@threecomma.group if you are holding one and have not heard.
  3. The intent stays readable. Poll GET /deposits/{id}, GET /withdrawals/{id}, GET /conversions/{id} or GET /payouts/{id}; the state moves to COMPENSATED when 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.

ScopeYour 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 chargedNothing. The refusal happens before anything moves, and the message says so.
Your positionUnchanged and still readable. GET /positions works throughout.
How it liftsA person at TCB establishes what happened and lifts it. Nothing lifts one on a timer.
What you should doStop 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.

Pilot disclosure

On this page