Skip to main content
The API is in preview until launch, so a change here can still remove or rename something. From launch on, changes follow the compatibility policy: additive only, with removals announced at least six months ahead.
Amount precision
  • An amount with more fractional digits than its asset has is refused before any quote or funding check, with 400 AMOUNT_PRECISION_EXCEEDED; details names the field, the decimals sent and the allowed precision (Conventions).
Firm only, and repeated signed reads
  • The search filter filters.executableOnly is renamed filters.firmOnly, and it now keeps only firm quotes (firm: true): executable estimates held to minOut, such as order-book and AMM quotes, are left out too, as are indicative quotes. A request still sending filters.executableOnly is refused with 400 INVALID_REQUEST (Firm only).
  • A signed read (GET or HEAD) sent again unchanged inside the 300-second window is accepted, so polling needs no nonce. A signed write is still accepted once (Sign requests).
  • An API key placing an order without an Idempotency-Key header is refused with 400 IDEMPOTENCY_KEY_REQUIRED (Idempotency).
  • The source refusal BINDING_NOT_ON_CHAIN is renamed WALLET_NOT_ON_CHAIN, with the same meaning (Wallets and accounts).
  • Every path that names a workspace calls its parameter workspaceId, for example GET /api/v1/workspaces/{workspaceId}/venue-enablements. The URLs themselves are unchanged.
  • A new source reason, venueEnablementRechecking: the venue’s readiness for your workspace is being re-checked, so it wasn’t asked. Request quotes again shortly (Error codes).
What an order did, one trade history, and exchange account balances
  • Every order, Basic and Advanced, carries what it actually did: fills, quotedBuyAmount and settledBuyAmount, the fees actually charged in fees (each with includedInPrice), the kinds whose amount was not recorded in unrecordedFees, and settlesBy (What the order did). Show an unrecorded fee as “not recorded”, never as 0.
  • The search filter filters.commitment is replaced by filters.executableOnly, a boolean: send true to leave indicative quotes out (Firm only). A request still sending filters.commitment is refused with 400 INVALID_REQUEST, details.field filters.commitment.
  • GET /api/v1/prices rows no longer carry commitment: they carry firm and lastLook, both always false (Prices before you quote).
  • GET /api/v1/workspaces/{workspaceId}/trades lists the workspace’s orders and exchange-account trades in one history, with cursor paging and CSV export. The key needs Read trades (Trade history).
  • GET /api/v1/workspaces/{workspaceId}/trades/{tradeId} reads one trade. A failed or refunded order’s settlement.failureReason says why (Read one trade). A trade read with an API key no longer carries the compliance records attached to it: read those in the app.
  • An API key with Read treasury reads an exchange account’s balances, deposit instructions and deposit history at …/venue-accounts/{venueAccountId}/balances, /deposit-instructions and /deposit-history; these used to answer 403 JWT_REQUIRED. A read whose authority is withdrawn while it runs answers 403 VENUE_READ_AUTHORITY_REFUSED (Exchange account balances).
  • GET /api/v1/workspaces/{workspaceId}/venue-enablements lists your exchange accounts with the same key, with cursor paging: each item’s accountId is the {venueAccountId} of those reads (List your exchange accounts). The deposit history is one bounded window, lookbackDays 1 to 30, with no cursor; more than 1,000 records answers DEPOSIT_HISTORY_WINDOW_TOO_LARGE.
Service-account tokens, key rules and response headers
  • A service account’s API key can be exchanged for a bearer token of at most 15 minutes, with the OAuth 2.0 client-credentials grant at POST /api/v1/oauth/token. GET /api/v1/me with the token answers for the key it was exchanged from: auth oauthClientCredentials, keyId, workspaceId, its live scopes and organisationWallets (Service accounts and tokens).
  • A key that requires request signing or holds venue trading is refused by the exchange with 400 unauthorized_client, error_uri ending #SIGNED_KEY_NOT_EXCHANGEABLE: sign each request with it instead.
  • Turning signing off on a key that can trade venues answers 409 VENUE_TRADE_KEY_SIGNING_REQUIRED; the key is unchanged.
  • A service account’s organisation-wallet trades stop with 409 SERVICE_ACCOUNT_SPONSOR_NOT_CURRENT when its sponsor is no longer a current member (Its sponsor).
  • Every answer to a key with a per-minute limit carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, refusals included. A deprecated operation’s answers will carry Deprecation, Sunset and Link; none is deprecated (Conventions).
  • Reading a quote set with GET /api/v1/quotes/requests/{quoteRequestId} moments after it was returned answers 503 QUOTE_REQUEST_NOT_READY instead of 404: read it again after about a second.
  • A venue-trading key that is revoked, expired or no longer current for its workspace is refused with VENUE_TRADE_KEY_NOT_CURRENT, and nothing is placed (Error codes).
  • Every quote carries firm (the venue is bound to the price until expiresAt) and lastLook, always false. Read firmness from firm and expiresAt: commitment said the same (How search works).
  • Quote cards no longer carry commitment, in POST /quotes and requotes alike: read firm and expiresAt.
  • Past a quote’s retainUntil, a requote or an order answers 410 REQUOTE_WINDOW_CLOSED with next.kind: "search": search again. 410 QUOTE_EXPIRED now means only that the quote lapsed and can still be requoted (Requotes and expiry).
A key reads its own organisation wallets; fuller filtered order pages
  • GET /api/v1/me with a workspace key answers organisationWallets: venueTradeScope, venueTradeEnabled, requestSigningRequired, the grants with each wallet’s financialAccountId, and blockedBy, the code an organisation-wallet order would be refused with right now (Check what a key can trade from).
  • GET /api/v1/orders with a status filter fills its page before answering, up to a scan limit. A short page with a nextCursor says scanLimitReached: true; a filter that matches nothing answers one page with no nextCursor.
The last pay and receive words become sell and buy
What to change:
  • Match the source reason buyAmountUnsupported where you matched receiveExactUnsupported, on quote sources, quote cards, price rows and netReceived.
  • Match sellChainInvalid and buyChainInvalid where you matched payChainInvalid and receiveChainInvalid.
  • Read sizeLimits.fixedAmount (sellAmount or buyAmount, the request field the limits bound) where you read sizeLimits.amountSide (payExact or receiveExact).
Refused orders, and on-chain orders that answer before signing
What to change:
  • Handle the order status refused, which is final (terminal: true). A pre-trade check refused the order before anything was sent, and operation.complianceRefusal names why. Such an order used to read pending. It cannot be confirmed, continued or cancelled: once the cause is cleared, request a new quote and place a new order. GET /api/v1/orders/by-quote/{quoteId} still answers ordered for its quote.
  • A Basic order on an on-chain venue now answers working as soon as it is recorded, before its wallet signs. A wallet that then refuses to sign ends the order unfilled, with operation.dispatch.notSentCause, instead of answering 409. Follow the order or its webhook to terminal.
  • Read outcomeUnknown only as a lost outcome. An on-chain trade waiting for its transaction to confirm reads working, not outcomeUnknown.
Advanced order detail in sell and buy
An Advanced order’s detail, the order of a kind advancedTrade answer, states its amounts as sell and buy, in decimals.What to change:
  • Read order.route.sell and order.route.buy, where you read the route’s pay and receive sides. Each carries its asset, chain and decimalAmount. When TetraFi cannot work out the decimals, the side carries unresolved (assetNotInRegistry or amountUnavailable) and no decimalAmount: decimals are never guessed.
  • Stop reading order.inputAmounts and order.outputAmounts. Outputs that pay a fee are listed in order.feeOutputs, in decimals.
  • Read each of order.legs[] as sell, buy (the leg’s estimate) and measuredBuy, with measuredBuySource, where you read inputAsset, inputAmount, outputAsset, outputAmount and measuredOutputAmount.
  • Read what was delivered from order.deliveredBuy, with deliveredBuyProvenance.
Prices in sell and buy
GET /api/v1/prices, GET /api/v1/prices/stream and every referenceMid use sell and buy too. The old names are gone, with no aliases.What to change:
  • Query prices with sell, buy and exactly one of sellAmount, buyAmount or sizes, where you sent pay, receive, amount and side. Pin chains with sell.chain and buy.chain, and filter variants with variants.sell and variants.buy. An old parameter is refused with 400 INVALID_REQUEST naming it.
  • Read the price response’s sell, buy and sellBalances, each row’s sellVariant and buyVariant, and each size’s sell and buy, where you read the pay and receive names.
  • Read referenceMid as sellUsd, buyUsd and buyPerSell, on prices and on quote sets, where you read payUsd, receiveUsd and receivePerPay.
Prices covers the request.
Company details before trading
A company, broker or liquidity provider workspace trades once its company details are on file: legal name, country of incorporation, registration number and registered address. TetraFi’s business verification and members’ identity verification no longer hold such a workspace back.What to change:
  • Expect 403 CAPABILITY_BLOCKED on quotes, orders and new counterparty connections while company details are missing. Its details.capabilityBlockers holds BASE_PROFILE_INCOMPLETE, with the missing fields in currentlyDue. Add them in the TetraFi app under Company profile.
  • Retry 503 CAPABILITY_UNAVAILABLE with the blocker BASE_PROFILE_UNREADABLE: the details could not be read just now.
  • KYC_NOT_VERIFIED now blocks only a direct user’s own workspace.
A workspace that cannot trade yet covers the refusal.
Venue account fields renamed
What to change:
  • On a venue account order (kind basicTrade), read operation.label where you read operation.accountLabel.
  • On a venue connection, read label and venueEnablementId where you read accountLabel and accountBindingId.
Sell and buy
A quote request names what you sell and what you buy, and fixes exactly one amount. The old pay, receive, amount and side fields are gone, with no aliases.What to change:
  • Send sell where you sent pay, and buy where you sent receive. Each is still {asset, chain?}.
  • Send sellAmount where you sent amount with side: "payExact", and buyAmount where you sent amount with side: "receiveExact". Both are decimal strings in Asset units.
  • Send exactly one of sellAmount and buyAmount. A body with both, or with neither, is refused with 400 INVALID_REQUEST naming them.
  • Send filters.variants as {"sell": …, "buy": …}, where it was {"pay": …, "receive": …}.
  • Read each quote’s sell and buy (asset, chain, variant, amount) where you read pay and receive. buy.amount is what lands at deliverTo.
  • Stop sending pay, receive, amount and side on POST /api/v1/quotes: they are unknown fields now, refused with 400 INVALID_REQUEST.
Requote and order bodies, and order answers, do not change. Sell exact or buy exact covers the new fields.
List and cancel orders
  • List your orders with GET /api/v1/orders: newest first, in the order envelope, filtered by status, kind, sellAsset, buyAsset, from and to, and paged with cursor. A page can hold fewer than limit orders, even none: follow nextCursor until it is null.
  • Cancel an order before it is sent to a venue, or a pay-in waiting for funds, with POST /api/v1/orders/{id}/cancel and an Idempotency-Key. Afterwards it answers 409 ORDER_NOT_CANCELLABLE with details.status. TetraFi never cancels at the venue or refunds.
  • GET /api/v1/workspaces/{workspaceId}/trades is no longer part of the integrator API: use the order list.
orderId on every order answer; minReceiveRequired on quotes
What to change:
  • Read an order’s id from the top-level orderId of the order answer, the status read and the recovery answer. It is the id GET /api/v1/orders/{id} takes, for Basic and Advanced orders alike.
  • Send minReceive when the quote’s minReceiveRequired is true, instead of working it out from commitment. A missing one is refused with 400 MIN_RECEIVE_REQUIRED, with details.field minReceive.
One order read, one 401 code, and renamed refusals
An order has one read, and every refusal has one code with one status.What to change:
  • Read every order at the statusUrl its answer gives: GET /api/v1/orders/{id}. It answers the same envelope as POST /api/v1/orders, for Basic and Advanced orders alike: kind, status, terminal, statusUrl, next when there is something to do, and the trade’s own detail under operation or order. The recovery answer of GET /api/v1/orders/by-quote/{quoteId} also carries statusUrl.
  • Read status from one camelCase list: pending, working, filled, settled, unfilled, cancelled, failed, refunded, reviewRequired and outcomeUnknown. Stop polling on terminal. While an order is not final, next says wait (read again after retryAfter) or contact (contact support with reference).
  • Stop building the Basic trade’s address from your workspace, connection and operation ids.
  • Match 401 AUTH_REQUIRED for every authentication failure, and read the cause in details.reason, for example MISSING_CREDENTIALS, INVALID_API_KEY or SIGNATURE_INVALID.
  • Match the renamed codes: QUOTE_PRICE_MOVED (was PRICE_MOVED_BEYOND_PROTECTION), VENUE_FIRM_QUOTE_LIMITED (was FIRM_QUOTE_LIMITED), QUOTE_ALREADY_ORDERED, QUOTE_ID_INVALID for a malformed quote id, and WALLET_SIGNATURE_REQUIRED for a missing wallet signature on an order.
  • Read docUrl on any refusal: it links the code’s entry in the error-code reference.
  • Deliver trade output to an address or one of your own accounts: no trade can deliver to a payee.
An API key implies its workspace
An API key is bound to the workspace it was created in, and every request it sends now works in that workspace without X-Workspace-Id.What to change:
  • Stop sending X-Workspace-Id with an API key, or keep sending it: when present it must be the key’s own workspace, or it is refused with 403 WORKSPACE_SCOPE_MISMATCH.
  • A signed-in member’s session still sends X-Workspace-Id. Without it, the request is refused with 400 MISSING_WORKSPACE.
  • Read a placed order’s status at the statusUrl its POST /api/v1/orders answer returns, instead of building the address yourself.
  • Filter GET /api/v1/workspaces/{workspaceId}/trades with its camelCase parameter names only, for example nextActor, dateFrom, amountMin and sortBy. The snake_case names are refused with 400.
Trading source and funding replace bind and binding
A quote request names what a trade executes with in source, and where its output goes in a top-level deliverTo. Each quote states its trading source and its funding. The old bind, binding and bindingOptions fields are gone, with no aliases.What to change:
  • Read sourceOptions where you read bindingOptions. It is listed on the quote set and on each quote. Candidates carry financialAccountId, kind, displayName and isDefault instead of id and label.
  • Send source.tradingWallets (CAIP-2 chain id to financialAccountId) where you sent bind.wallets, source.venueAccountId where you sent bind.account, and source.fundingAccountId where you sent bind.bankAccount.
  • Send deliverTo at the top level of the request or requote, not under bind. Its kinds are financialAccount, payee and address.
  • Read each quote’s tradingSource, fundingModel, fundingAccount, settlement and deliverTo where you read binding. A quote with no usable source carries sourceRefusal.
  • Read a quote’s location as { "kind": … } only: singleChain, crossChain, offChain or mixed.
  • On a walletRefused source, read recovery.defaultWallets and send it as source.tradingWallets, where you sent recovery.defaultBinding as bind.
  • Match SOURCE_NOT_OWNED (403) where you matched BINDING_NOT_OWNED. It now refuses a deliverTo or source.fundingAccountId naming a wallet or account you cannot use.
  • A trading wallet in source.tradingWallets that a venue account has not linked is no longer refused. That venue reads unavailable with the reason tradingSourceNotLinked, and the others still quote.
The other source refusal codes keep their names, for example BINDING_NOT_ON_CHAIN. Wallets and accounts covers the new fields.
Consistent refusals, order status and source reasons
Every refusal of a quote, price, order, preflight, asset or order-status request, at every status, is now one JSON object:What to change:
  • Read requestId where you read traceId before.
  • Read waits from details.retryAfter, an ISO 8601 duration such as PT2S. The Retry-After header still carries the same wait in seconds.
  • Fields a refusal used to send at its top level now sit under details.
  • A request body that is too large now answers JSON, 413 REQUEST_BODY_TOO_LARGE, instead of plain text.
  • A missing or empty X-Workspace-Id answers 400 MISSING_WORKSPACE everywhere. A malformed one answers 400 INVALID_WORKSPACE_ID.
  • Send credentials on GET /api/v1/orders/{orderId}. Without them, you get 401 MISSING_CREDENTIALS before any order lookup. This one is rolling out: until it reaches a deployment, a read without credentials answers 404 ORDER_NOT_FOUND.
  • Read order status as a string, including "failed". Failure details are in failure.phase, failure.code and failure.message, not inside status.
  • Source reasons on quotes and prices are camelCase tokens, for example pairNotServed instead of pair_unsupported and apiKeyOrganisationWalletScopeRequired instead of API_KEY_ORGANISATION_WALLET_SCOPE_REQUIRED.
  • Match receiveExactUnsupported when a source cannot quote a fixed receive amount. Quotes and prices now use the same token. Read it from reason, or from detail on a filteredOut row.
The error-code reference lists every declared refusal and source reason. The Idempotency-Key rules for POST /api/v1/orders are now documented in full: see Idempotency.
New developer docs
These docs now describe one API for search, quotes, requotes and orders, with a runnable sample on every page. They start from the Quickstart.The separate RFQ API, Router API, Price API and Trade History API pages are gone. Their old addresses redirect to the pages that replace them.
Venue identity on quotes
Each quote leg and each entry in sources names the venue it comes from with venueId and venueName. Group sources and quotes by venueId for display. The existing id still identifies the route that executes, and it is the id filters.venues takes (Venue types and venues). Pools and solvers that are not a named venue leave both fields out.
Quotes by asset pair, requotes, and orders by quote id
A quote request names the asset you pay and the asset you receive, not chains or wallets: POST /api/v1/quotes with pay, receive, amount and side. Every quote states where it executes and how firm it is.
  • Changing a quote’s wallet, account or route, or turning an indicative price executable, is a requote: POST /api/v1/quotes with requote set to the quote id. See Requotes and expiry.
  • POST /api/v1/orders takes the quote id. The server holds the quote’s terms, so you never send them back. See Orders.
  • GET /api/v1/assets lists what your workspace can trade, and GET /api/v1/prices returns indicative prices without asking any venue for a quote.