The Transactions API is 23 endpoints. Most integrations use five of them.
This page is the map: what each endpoint is for, what state a transaction must be in before you call it, and which endpoints exist only for a specific wallet model. If you have not created a transaction yet, start with Your First Transaction, which walks one payment end to end.
The five you will actually use
| Endpoint | When |
|---|---|
POST /transactions/quote |
Before creating, to lock fees for 120 seconds |
POST /transactions |
Create the transaction |
GET /transactions/{id} |
Poll state, or reconcile after a webhook |
POST /transactions/{id}/hash |
RAW only: report the transfer hash |
POST /transactions/{id}/refund |
Return funds to the sender |
Everything else is either a read, a batch equivalent, or a release variant tied to a particular wallet model. The rest of this page explains which is which.
Create and quote
POST /transactions/quote
Returns a quote with the platform fee, workspace fee, gas pass-through where it
applies, and the total charged. The quote has a 120-second TTL. Pass the
returned quoteId to POST /transactions to lock those numbers in.
Without a quoteId, fees are computed at creation time and may differ from
anything you showed the user. If your UI displays a total before a confirm
step, quote first.
POST /transactions
Creates the transaction. The response status tells you what has to happen
next, and it is not always pending:
pending— nothing to do; the platform proceedsrisk_review— compliance screening is runningpending_user_action— a wallet must sign before funds move
Fee previews
Use POST /transactions/quote, which returns a lockable quoteId alongside
the fee breakdown. The older fee-preview endpoint is no longer part of the
documented surface — it still answers on v1 until that version sunsets, but
there is no v2 equivalent and nothing new should call it.
Reads
| Endpoint | Returns |
|---|---|
GET /transactions |
Paginated list with filters |
GET /transactions/{id} |
One transaction, full detail |
GET /transactions/stats |
Aggregate counts and volumes |
GET /transactions/template-config |
Sanitized control config for a template |
GET /transactions/template-config deserves a note. It returns public-safe
summaries of each active control on a template, for rendering a
“what protects this payment” panel before the user confirms, and for enforcing
client-side amount limits. It is deliberately sanitized: only a hand-picked
subset of each control’s config is exposed, never policy or implementation
fields. Do not expect it to describe how a control decides anything.
Reporting an on-chain hash
Which endpoint you call depends on the orchestration type, and they are not interchangeable:
| Orchestration | Endpoint | What it reports |
|---|---|---|
RAW |
POST /transactions/{id}/hash |
A direct wallet-to-wallet transfer |
COINBAX (external wallet) |
POST /transactions/{id}/escrow-hash |
Funds deposited into the escrow contract |
RAW has no contract and no hold period. COINBAX uses the escrow contract
and a time delay. Calling the wrong one is a 400.
Release: five endpoints, one decision
This is the part of the API most likely to send you to the wrong endpoint. All
five exist because who broadcasts the on-chain write and who pays gas
vary, and the escrow’s PRE_RELEASE gate has to be satisfied before any of them
succeed.
Start here:
Is the hold window over?
├── YES ─ Who broadcasts?
│ ├── Coinbax ───────▶ POST /transactions/{id}/release
│ └── Your wallet ────▶ POST /transactions/{id}/release/attest
│ then your own on-chain releaseAfterDelay
└── NO (early release, sender must consent)
├── Sender signs, relayer pays gas:
│ 1. POST /transactions/{id}/release-early/attest → EIP-712 payload
│ 2. POST /transactions/{id}/release-early → submit signature
└── Sender's own wallet broadcasts:
POST /transactions/{id}/release/attest
then your own on-chain releaseEarly
POST /transactions/{id}/release
Coinbax broadcasts releaseAfterDelay using a gas-only server signer. Runs the
PRE_RELEASE controls, records the on-chain attestation, then releases. The
payout goes to the fixed on-chain receiver — the server only pays gas, so
this stays non-custodial.
Rejected before the window elapses. Early release must be signed by the sender; there is no way around that.
POST /transactions/{id}/release/attest
Use when your own wallet will broadcast the release. Runs the PRE_RELEASE
controls and records the aggregate result on-chain so your subsequent
releaseEarly or releaseAfterDelay write does not revert on the escrow’s
PRE_RELEASE gate.
Call it immediately before your on-chain write. It authorizes; it does not release.
POST /transactions/{id}/release-early/attest then /release-early
Gasless early release. The first call runs the PRE_RELEASE controls and, only
if they pass outright, returns an EIP-712 payload for the sender to sign — no
transaction, no gas. The second submits that signature, and the relayer
broadcasts and pays gas.
The first call has three outcomes, and only one of them yields a payload:
status |
Meaning |
|---|---|
ready |
Payload returned; have the sender sign it |
under_review |
A control is still pending. No payload. Retry later |
rejected |
A required control failed; the escrow auto-refunded on-chain |
Two constraints worth knowing before you build on this: it is feature-flagged (403 when disabled) and v1 supports only bare external-wallet EOA senders. Authorizations expire, and controls are re-checked fail-closed before the relayer broadcasts, so a stale signature is a 400 rather than a silent no-op.
Ending a transaction early
| Endpoint | Precondition | On-chain? |
|---|---|---|
POST /transactions/{id}/cancel |
PENDING_USER_ACTION |
No — funds were never escrowed |
POST /transactions/{id}/rescind |
Within the hold period, sender only | Depends on the signing wallet — see below |
POST /transactions/{id}/refund |
Escrowed | Yes |
POST /transactions/{id}/release |
Escrowed, hold window elapsed | Yes — releases to the recipient |
cancel is for the user rejecting the wallet popup with no intent to retry. It
marks the transaction FAILED with userCancelled=true and rejects with 400
once the transaction reaches ESCROWED — at that point use refund or
rescind.
Rescind comes from a control, not from this list
rescind is in the table above because it ends a transaction, but that is not
where it comes from. A TimeDelay control grants the sender the right to pull
a payment back during the hold window. Rescind is the sender exercising that
right — configure the control and the capability appears; leave it off and there
is nothing to call.
That origin explains every one of its conditions:
- the control must exist on the transaction, and have
allowSenderRescindenabled - the call must fall inside the hold window the control defines
senderWalletmust match the escrow’s sender — a mismatch is a403, not a400- if the control sets
rescindRequiresReason, areasonis mandatory
Who signs determines what the API call does. The escrow contract only lets
the wallet that funded an escrow pull it back — cancelEscrow requires
msg.sender to be the original sender — so the answer follows from which wallet
created the escrow:
| The escrow was funded by | POST /transactions/{id}/rescind does |
|---|---|
| A wallet Coinbax operates for you (via your configured wallet provider) | Records the rescind and submits the on-chain call, because that wallet is the escrow’s sender |
| Your own wallet (MetaMask, Send, Banno) | Records the rescind only — status, metadata, webhook. Your wallet then calls cancelEscrow |
In both cases the funds move under the authority of the wallet that owns them. Coinbax operates a wallet on your behalf only where you have configured a wallet provider to do so; it never takes custody of funds and cannot move an escrow your own wallet created.
See Controls for configuring TimeDelay.
Batches
Multi-recipient sends mirror the single-transaction endpoints:
| Endpoint | Purpose |
|---|---|
POST /transactions/batches |
Create a parent batch plus one leg per recipient |
GET /transactions/batches |
List batches (legs not embedded) |
GET /transactions/batches/{id} |
One batch, legs included |
POST /transactions/batches/{id}/cancel |
Cancel an unsubmitted batch |
POST /transactions/batches/{id}/escrow-hash |
Report the batchCreateEscrow hash |
Batch 2FA verification is not in the documented surface: it routes to a
control that is not in the catalog, and TwilioSMS is the real one. Verify a
batch the same way you verify a single transaction — see
Controls.
Creation writes the parent and every leg in one database transaction. Your
client then signs a single batchCreateEscrow call using each leg’s UUID as the
on-chain transactionId. Per-leg escrow events then advance each leg
independently — so legs can end in different states, and a batch is not
atomic after submission.
To list batches and show recipients, you need GET /transactions/batches/{id}
per batch; the list endpoint omits legs deliberately.
Two things that surprise people
amount is a number in requests and a string in responses. The API accepts
amount as a JSON number, but the column is a SQL decimal, and the Postgres
driver returns decimals as strings to avoid float precision loss. So:
// request
{ "amount": 100.5 }
// response
{ "amount": "100.500000", "fee": "0.250000" }
Do arithmetic with a decimal library, and never === a response amount against
a number literal. The same applies to fee, netAmount, and other monetary
fields.
Release does not always mean released. A POST /transactions/{id}/release can legitimately
return 200 when the escrow was already released or refunded on-chain (the
platform syncs its record), or when a required PRE_RELEASE control failed and
the escrow was refunded instead. Read the response rather than assuming 200
means funds moved to the recipient.
Related
- Your First Transaction — one payment, end to end
- Controls — what runs inside the lifecycle
- API Scopes — releasing funds needs
move:transactions, reversing themreverse:transactions; neither is granted bywrite:transactions - Webhooks — react to state changes instead of polling
- Errors — the response envelope and error codes