Guides

The Transactions API

Every public transaction endpoint, grouped by the job it does, with the state each one expects and the pitfalls that are easy to hit.

Last updated 2026-08-28View as Markdown

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 proceeds
  • risk_review — compliance screening is running
  • pending_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 allowSenderRescind enabled
  • the call must fall inside the hold window the control defines
  • senderWallet must match the escrow’s sender — a mismatch is a 403, not a 400
  • if the control sets rescindRequiresReason, a reason is 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.

  • Your First Transaction — one payment, end to end
  • Controls — what runs inside the lifecycle
  • API Scopes — releasing funds needs move:transactions, reversing them reverse:transactions; neither is granted by write:transactions
  • Webhooks — react to state changes instead of polling
  • Errors — the response envelope and error codes