API Conventions
These conventions apply uniformly to every endpoint in the server-to-server API (/api/v1).
Response envelope
| Scenario | Shape |
|---|---|
| Success — single resource | { "data": { ... } } |
| Success — paginated list | { "data": [ ... ], "meta": { ... } } |
| Failure | { "code": "business_code", "message": "human-readable", "data": null } |
Pagination
All list endpoints accept the following query parameters:
| Query | Type | Default | Bounds |
|---|---|---|---|
page | int | 1 | ≥ 1 |
per_page | int | 25 (orders & transactions: 50) | 1–100 |
meta shape:
{ "current_page": 1, "last_page": 8, "per_page": 25, "total": 192 }
Money & time
- Money: always a decimal string (e.g.
"100.00") paired with acurrencycode (e.g."USD"). Never use floating-point arithmetic when processing amounts. - Time: UTC, ISO 8601 (e.g.
2026-06-08T01:00:00+00:00).
Rate limiting
Two-axis throttle:
- Each token has its own per-minute ceiling (
rate_limit_per_minute), configured when the token is created. - Each IP has a coarser cross-token ceiling (default 600 req/min).
Successful responses carry:
X-RateLimit-Limit— the token's per-minute ceilingX-RateLimit-Remaining— remaining requests in the current window
Exceeding the limit returns 429 api.rate_limited with a Retry-After: <seconds> header. Back off exactly by that duration.
Idempotency
All write endpoints accept an optional X-Idempotency-Key header (≤ 200 chars).
| Scenario | Behaviour |
|---|---|
| First request with a given key | Executes and stores the response |
| Replay: same key + same body (within 24 h) | Returns original result with Idempotent-Replayed: true; no side effects |
| Same key + different body | 422 api.idempotency_key_conflict |
| Key exceeds 200 chars | 422 api.idempotency_key_invalid |
5xx response | Not stored — safe to retry |
Strongly recommended: send a unique
X-Idempotency-Keyon every write (e.g. your own order ID). This prevents double-charge or double-issue when the network retries a request.
Error code reference
| HTTP | Code | Meaning |
|---|---|---|
| 401 | api.unauthenticated | Token missing / invalid / expired / disabled |
| 403 | api.ip_not_allowed | Calling IP not in the token allowlist |
| 403 | api.insufficient_ability | Token lacks the ability this endpoint requires |
| 403 | api.workspace_inactive | The owning workspace is not active |
| 402 | wallet.insufficient_balance | Insufficient wallet balance (issue / top-up) |
| 404 | *.not_found | Resource not found in this workspace (e.g. card.not_found, order.not_found) |
| 409 | (state conflict) | The resource's current state forbids the action (e.g. double freeze) |
| 422 | api.idempotency_key_invalid | Idempotency key too long |
| 422 | api.idempotency_key_conflict | Idempotency key reused with a different body |
| 422 | (validation) | Request parameters or business rules failed |
| 429 | api.rate_limited | Rate limit exceeded; back off per Retry-After |