Conventions
The response envelope, cursor pagination, errors, idempotency, rate limits, and versioning.
The response envelope
Every response is JSON with one of exactly two shapes. Success carries a data payload plus
optional meta; error carries a single error object (never both).
{ "data": { "…": "the resource, or an array of resources" }, "meta": { "…": "pagination etc." } }{ "error": { "code": "validation_error", "message": "Human-readable summary.", "details": [] } }- Money is always an integer in minor units (
*_minor, e.g. pence for GBP) paired with an ISO-4217currency— never a float.1050+"GBP"means £10.50. - IDs are UUIDv7 strings; timestamps are RFC 3339 UTC.
- Branch on the machine-stable
error.code, never onmessage.
Cursor pagination
List endpoints use an opaque cursor, not an offset. Request with limit (default 25, max
100) and pass the meta.cursor.next token back verbatim for the next page. When
meta.cursor.has_more is false, you have reached the end. Do not construct or parse a cursor.
Error code taxonomy
The code is the contract (HTTP status is aligned but secondary). It is stable and additive.
code | HTTP | Retry? |
|---|---|---|
invalid_request | 400 | No |
unauthorized | 401 | No |
forbidden / insufficient_scope | 403 | No |
not_found | 404 | No |
conflict / idempotency_conflict | 409 | Sometimes |
validation_error | 422 | No — fix the fields |
rate_limited | 429 | Yes, after Retry-After |
internal_error / service_unavailable | 500 / 503 | Yes — backoff |
Idempotency
Every mutating endpoint accepts an Idempotency-Key header. The first request's response is
captured; any repeat with the same key replays the stored response byte-for-byte (the operation
runs at most once). Reusing a key with a different body returns 409 idempotency_conflict. Use a
fresh UUID per logical operation.
Rate limits
Each credential is metered by a per-key token bucket. Every response carries
X-RateLimit-Limit / -Remaining / -Reset; a 429 includes Retry-After. Back off on 429,
cache reference data, and prefer webhooks over polling.
Versioning and deprecation
The URL carries the major version — /v1. Additive, backward-compatible changes (a new
endpoint, a new optional field, a new enum value, a new webhook event type, a new error code)
ship in place under /v1 with no notice required — build clients to tolerate unknown fields and
enum values; a client that hard-fails on one it doesn't recognise is the bug. A breaking change
(removing or renaming a field, changing a type or default behaviour, tightening validation) only
ever ships as a new major version, /v2, introduced alongside a still-serving /v1 —
never in place.
A deprecated endpoint or version returns machine-readable headers ahead of an announced sunset
date — Deprecation (RFC 9745), Sunset (RFC 8594), and a Link: rel="deprecation" to its
migration guide — alongside a changelog entry. After the Sunset date the
endpoint returns 410 gone.
Webhook payloads version independently of /v1, via each endpoint's own pinned api_version —
see Webhooks.
This guide mirrors the canonical source in docs/api/conventions.md and docs/api/lifecycle.md.