> ## Documentation Index
> Fetch the complete documentation index at: https://tetrafi.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Compatibility policy

> What can change in the API after launch, what never changes without notice, and how your client stays compatible.

This policy covers the integrator API: the operations in
[API reference](https://tetrafi.mintlify.site/reference/introduction.md), 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](https://tetrafi.mintlify.site/changelog.md).

## 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:

```json theme={null}
{
  "error": "QUOTE_EXPIRED",
  "message": "This quote has expired. Request a new one.",
  "requestId": "r1-mvfqiqp1-0",
  "timestamp": 1792443399,
  "next": { "kind": "requote", "needs": [] },
  "details": { "retryAfter": "PT2S" }
}
```

* `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.

| status | Meaning | FIX OrdStatus analogue | terminal |
| - | - | - | - |
| `pending` | Accepted, nothing sent yet | A PendingNew | no |
| `working` | Sent; executing | 0 New / 1 PartiallyFilled | no |
| `filled` | Executed; settlement or delivery remains | 2 Filled | no |
| `settled` | Executed and settled, or no settlement applies | 2 Filled | yes |
| `unfilled` | Venue did not fill; nothing traded | 4 Canceled / C Expired | yes |
| `cancelled` | Cancelled before dispatch | 4 Canceled | yes |
| `failed` | Refused or failed; nothing more will happen | 8 Rejected | yes |
| `refunded` | Funds returned to the payer | 4 Canceled | yes |
| `reviewRequired` | A person must review before it continues | 9 Suspended | no |
| `outcomeUnknown` | Venue outcome is being reconciled | none | no |
| `refused` | Refused by a pre-trade check before anything was sent; `operation.complianceRefusal` names why. Trade again with a new quote | 8 Rejected | yes |

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.