Conventions
The rules below describe the /api/v1/* surface. Where a rule is narrower than
that — idempotency and the page envelope both are — it says so.
Idempotency
Section titled “Idempotency”Idempotency-Key is honoured on /api/v1/oms/* — the whole OMS router,
which is every write that can move a position. Elsewhere on /api/v1/* the
header is accepted and ignored, so treat a retry on those routes as a second
write, not a replay.
Where it applies, the key is 8–200 characters:
- Replaying the same key with an identical payload returns the cached
response, with
Idempotency-Replay: true. The window is 24 hours. - The same key with a different payload returns
409(idempotency_key_conflict). - A key outside 8–200 characters returns
400(invalid_idempotency_key).
curl -s https://api.troncharts.xyz/api/v1/oms/intents \ -H "authorization: Bearer $TOKEN" \ -H "x-tenant-slug: $TC_TENANT_SLUG" \ -H "idempotency-key: $(uuidgen)" \ -H 'content-type: application/json' \ -d '{ … }'Send one on every order write. A network timeout on placement is otherwise indistinguishable from a rejection, and retrying without a key can double a position.
Pagination
Section titled “Pagination”Most list endpoints are cursor-paginated.
?limit=<n>— default 100, and the cap is per endpoint: 500 on most, including/api/v1/accounts/{id}/events; 1000 on the/api/v1/accounts/{id}history routes/orders,/trades,/fills,/order-historyand/funding; 5000 (default 1000) on/api/v1/accounts/{id}/state/history.?cursor=<opaque>— take it from the previous response.
Every cursor-paginated list response carries nextCursor, and a null
nextCursor means you have reached the end. Treat the cursor as opaque; it is
not an offset and its encoding is not part of the contract.
Not every list route is cursor-paginated. /api/v1/accounts/{id}/state/history
is window-based: it returns a boolean truncated and no cursor at all, so widen
from/to or raise limit rather than paging it. Small collections such as
/api/v1/accounts and /api/v1/accounts/{id}/positions are returned whole.
The array key is not uniform. The account history routes name the array
after the resource and return no hasMore:
| Endpoint | Array key |
|---|---|
/api/v1/accounts/{id}/orders |
orders |
/api/v1/accounts/{id}/trades |
trades |
/api/v1/accounts/{id}/fills |
fills |
/api/v1/accounts/{id}/funding |
events |
Other list endpoints do carry hasMore, but the array key still varies:
/api/v1/sor/decisions uses data, while /api/v1/reports/* also names the
array after the resource (orders, fills, positionsClosed, funding). Take
the shape from the endpoint’s own reference page rather than assuming one
envelope, and on a cursor-paginated route page until nextCursor is null:
let cursor: string | undefineddo { const url = new URL(`https://api.troncharts.xyz/api/v1/accounts/${accountId}/trades`) url.searchParams.set('limit', '200') if (cursor) url.searchParams.set('cursor', cursor)
const res = await fetch(url, { headers: { authorization: `Bearer ${token}`, 'x-tenant-slug': tenantSlug }, }) const page = (await res.json()) as { trades: unknown[]; nextCursor: string | null }
handle(page.trades) cursor = page.nextCursor ?? undefined} while (cursor)Rate limits
Section titled “Rate limits”Limits are per credential and configured per deployment. When you exceed one
you get 429 with a Retry-After header — honour it rather than backing off
on a fixed timer. The bootstrap path (POST /api/auth/bootstrap) is hard-capped
at 10 requests per minute per API key.
Decimals
Section titled “Decimals”Sizes and prices are accepted as decimal strings or numbers, and the server
coerces. Prefer strings: JSON numbers are IEEE-754 doubles, and a size like
0.1 does not survive the round-trip exactly. The platform reads money as
decimals end to end — match it.
Errors
Section titled “Errors”Failures return a JSON body with a stable machine-readable error code, usually
alongside a detail:
{ "error": "venue_rejected", "detail": "insufficient margin" }detail is best-effort, not guaranteed: some failures carry none at all, and on
validation errors it is a structured array of field issues rather than a
sentence. Never require it to be present or to be a string.
| Status | Means |
|---|---|
400 |
Malformed request — a field is missing or the wrong shape. |
401 |
No credential, or an expired / revoked token. |
403 |
Authenticated but not allowed — wrong scope, wrong tier, or another tenant’s resource. |
404 |
The request resolved to no tenant (unknown_tenant_origin). Send X-Tenant-Slug or call from a registered origin — see Tenancy. |
409 |
Conflict — idempotency-key reuse with a different payload, or a state transition that isn’t legal. |
410 |
The tenant is suspended (tenant_suspended). |
422 |
The venue rejected the order. detail carries the venue’s reason. |
429 |
Rate-limited. Read Retry-After. |
A 404 is the one to read carefully: it far more often means “you did not
identify a tenant” than “that endpoint does not exist”. Check the error code
before concluding a route was removed.
Branch on error, never on detail — the code is contract, the prose is not.