Skip to content

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-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).
Terminal window
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.

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-history and /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 | undefined
do {
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)

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.

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.

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.