Skip to main content
This policy covers the integrator API: the operations in API reference, which are asset discovery, prices, quotes and requotes, orders, and the order and quote-set reads. It takes effect at launch. Until then the contract is still changing, and every change is listed in the changelog.

Reference exposure and launch availability

GET /api-docs/openapi.json is the public integrator projection generated at docs/api/integrator/openapi.json, byte-for-byte. Launch annotations appear only after the export owner publishes them; the runtime does not patch the artifact. It never serves the full platform API as a fallback. GET /api-docs/full/openapi.json requires a current platform-admin session with verified MFA, and is not available to an API key or a workspace administrator without that platform grant. Its response is private, no-store. Operations that are not launched are left out of the projection; they return when they launch. Leaving one out does not grant or revoke runtime execution authority. UI and SDK generation continues to consume committed full-spec files, not these HTTP routes.

What we promise

After launch, a change to the integrator API is additive. We may:
  • add an operation, an optional request field, a query parameter or a header;
  • add a field to a response object;
  • add a value to a response enum or a tagged union, or a new error code;
  • add a status code to an operation, as long as it uses the error body below;
  • make a request rule looser, for example by accepting a value that used to be refused.
We do not, without the deprecation process below:
  • remove or rename an operation, field, value, header or code;
  • change a field’s type or meaning, or a code’s HTTP status;
  • make an optional request field required, or a request rule stricter;
  • stop sending a response field we documented as always present.
Before launch, a breaking change lands in place with no alias and a CHANGELOG entry. The latest is the sell/buy vocabulary of 2026-10-31. It renamed fields across quotes, prices, orders, trades and fills. It also renamed enum values: sizeLimits.amountSide (payExact/receiveExact) became sizeLimits.fixedAmount (sellAmount/buyAmount), and the reasons receiveExactUnsupported, payChainInvalid and receiveChainInvalid became buyAmountUnsupported, sellChainInvalid and buyChainInvalid.

What your client should do

  • Ignore fields you don’t know. Response objects can gain fields at any time, so don’t validate them as closed.
  • Treat unknown enum values as “other”. Every response enum and tagged union in the spec is marked x-extensible-enum. Handle the values you know, and give anything else a neutral fallback. For example, a quote source with an unknown status is a source you can’t use. A next action with an unknown kind is a step you can’t take automatically.
  • Handle unknown error codes by status. Act on the codes you know, and treat any other code by its HTTP status class: retry a 429 or 503 after Retry-After, fix the request on any other 4xx, and retry a 5xx with backoff.
  • Send only documented request fields. Request bodies stay strict: an unknown field answers 400 INVALID_REQUEST naming it, so a typo never passes silently.
  • Discover values instead of hard-coding them. Assets, chains and venues come from GET /api/v1/assets and from quote responses. A venue or asset can be added without any change to your code.

Order identity and minimum receive

Both order kinds carry one top-level orderId, accepted by GET /api/v1/orders/{id}. An Advanced order retains order.orderId; a Basic order retains operation.operationId. The ordered by-quote recovery answer and order.updated.data carry that same canonical id. These nested resource identifiers are not aliases for a second order resource. Read each quote card’s minReceiveRequired boolean before ordering. When true, supply minReceive; omission returns 400 MIN_RECEIVE_REQUIRED with details.field: "minReceive". The flag describes existing admission, not a new restriction. A false flag does not grant permission to order: readiness, expiry and the card’s next still apply.

The error body

Every refusal of an integrator operation, at every status, is one JSON object:
  • error is the stable code. Match on it, never on message.
  • message is for people and may change.
  • requestId equals the X-Request-Id response header. Quote it when you contact support.
  • next, when present, is the way forward.
  • details, when present, carries the code’s detail. Its keys are camelCase and it can gain keys. On 429 and some 503 answers, details.retryAfter is an ISO 8601 duration, and the Retry-After header carries the same wait in seconds.
The authentication, rate-limit, body-size and timeout refusals use this same body. One parser reads them all.

Retries and idempotency

POST /api/v1/orders takes an Idempotency-Key header, and API keys must send one. A retry with the same key and the same body returns the first answer and never places a second order. A different body under the same key answers 409 IDEMPOTENCY_CONFLICT. Refusals are not stored, so after one, fix the request and send it again. The OpenAPI description of the header has the full rules.

Cancellation

POST /orders/{id}/cancel accepts the GET order identifier and returns that same envelope with status: "cancelled" and terminal: true. It is naturally idempotent by order id: a repeated cancellation does not change the resource. Sessions need PlaceOrder; keys need orders_write and Idempotency-Key. Only undispatched Basic orders and deposit-first pay-ins still awaiting funds are eligible. A durable claim or any progressed funding/delivery step refuses with 409 ORDER_NOT_CANCELLABLE; details.status is the current public lifecycle status. Advanced orders are not cancellable through this endpoint. No cancellation calls reach a venue, and any deposit already credited remains the workspace’s balance.

Order lifecycle

POST /api/v1/orders and GET /api/v1/orders/{id} return the same envelope: {kind, status, terminal, statusUrl, quoteId?, requestId?, order|operation}. kind is advancedTrade (nested order) or basicTrade (nested operation). For Basic trades, {id} is the operation id. statusUrl is always /api/v1/orders/{id}, including quote-order recovery and webhook data. A workspace reader with ViewOrders or PlaceOrder can read a placed order; API keys need orders_read. Unknown, foreign and unreadable orders share 404 ORDER_NOT_FOUND; a key missing the read scope receives 403 INSUFFICIENT_SCOPE. States may be skipped; act on terminal, not on a particular sequence. The public status is an open, camelCase enum; nested native status fields retain their own vocabulary. Terminality comes from the order’s durable product facts. Advanced escrow, resource-lock and deferred orders can still owe a claim after native settled; route delivery and atomic venue orders need no separate claim. A failed fill that can recover remains nonterminal. Basic orders require final execution and final settlement/delivery, or no such legs. Review and unknown outcomes never mean terminal. For an uncertain or review-required order, next names the supported step: wait carries an ISO 8601 retryAfter for another read; contact carries the order reference for support. Neither authorizes another order or a funds movement. Terminal orders carry no next. Unknown future action kinds are not actions a client may take automatically.

Webhooks

A workspace webhook receives a signed POST for each event it subscribes to. order.updated is sent on every status change of an order placed in that workspace.
  • Body. {id, type, apiVersion, createdAt, data}. For order.updated, data is {orderId, kind, status, terminal, statusUrl}; read the order at statusUrl for its full state. If durable lifecycle facts are temporarily unavailable, terminal is omitted (never guessed) and status names the observed event. New type values and new fields can appear without notice, so ignore what you don’t know. A breaking change to a body ships under a new apiVersion.
  • Signature. Standard Webhooks headers: webhook-id, webhook-timestamp (Unix seconds), and webhook-signature: v1,<base64>. Each signature is HMAC-SHA256 over "<webhook-id>.<webhook-timestamp>.<raw request body>", keyed with the base64-decoded suffix of the shown-once whsec_<base64> secret. Verify the raw body with a Standard Webhooks library and reject timestamps more than five minutes from now before processing the event. Rotation dual-signs with current and previous keys for 24 hours: space-separated v1,<base64> values, either of which may verify. No legacy headers are sent. Existing sealed key bytes are unchanged. For an older hex secret, use "whsec_" + Buffer.from(oldSecret, "hex").toString("base64") in the reference library.
  • Delivery. At least once: a delivery that does not get a 2xx is retried after 5 s, 30 s, 10 min, 1 h, 6 h and 24 h. Deduplicate on webhook-id, and on the body’s id for the event itself. The delivery log and the test call are in the webhook endpoints.

Deprecation

When something has to go after launch:
  1. The changelog announces it, with a removal date at least 6 months away. A security fix may need a shorter window, and the notice says why.
  2. Every affected response carries a Deprecation header (RFC 9745), a Sunset header with the removal date (RFC 8594), and a Link: <…>; rel="deprecation" header pointing to the notice. The spec marks the operation or field deprecated: true.
  3. The replacement ships before the notice, so you can move at any point in the window.
  4. On the sunset date, the old shape is removed.

How we check ourselves

Each build regenerates the integrator spec and compares it with the spec published at the last release. Any breaking difference by the rules above fails the build. Until launch the check only reports, because the contract is still changing in place.