docs
API

Rate limits, errors and versioning

The operational contract: limits and their headers, idempotent retries, the error catalogue, and how the API versions.

Rate limits

Every key may make 25 requests per second. The limit is a token bucket that refills all the time. Picture a bucket of 25 tokens: each request takes one, and tokens trickle back in steadily. So short bursts above the rate are fine, as long as the average holds.

Every /v1 response tells you where you stand:

HeaderMeaning
X-RateLimit-LimitThe bucket size (25)
X-RateLimit-RemainingRequests left right now
X-RateLimit-ResetUnix seconds until the bucket is full again

Past the limit you get a 429 with Retry-After in seconds. Honour it and back off. Under any normal load, the bucket refills within a second. Limits are per key, so a busy integration can have a key of its own.

There's also a limit per network address, checked before your key is: 120 requests at once, refilling at 60 a second. One integration never gets near it. It's there to stop a script that's guessing keys. It sends the same 429 with Retry-After, without the X-RateLimit-* headers.

Idempotent retries

An idempotent request is safe to send twice: the work still happens once. To make a POST idempotent, send an Idempotency-Key header. It can be any string up to 255 characters.

  • Any retry with the same key gets the first response again, for 24 hours. It's marked Idempotent-Replayed: true.
  • Reusing a key with a different request is a 422. Nothing is silently overwritten.
  • Retrying while the first request is still running is a 409 with Retry-After: 1. So even a racing retry can't run the work twice. Wait a moment and the retry returns the stored result.
  • A failed request releases its key, so you can fix the body and try again.
  • Keys are scoped to your API key, so two systems using plain counters never collide.

Use it on anything that must not happen twice. For example, creating sales, partners or payout batches on a network you don't trust to deliver your response.

Errors

Every error has the same shape:

{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "message": "limit must be an integer between 1 and 100",
    "param": "limit"
  }
}

param names the parameter at fault, when there is one. There are nine types:

TypeStatusWhen
authentication_error401Missing, malformed, unknown or revoked key
permission_error403The key lacks the scope; the message names the one it needed
invalid_request_error400, 422A parameter or body that doesn't parse or validate; a reused idempotency key
not_found_error404No such object in your organization (resource_missing)
conflict_error409The write contradicts current state
rate_limit_error429Over 25 requests per second
plan_error402The account's plan has lapsed; the key is paused, nothing is deleted
not_implemented_error501A documented capability not switched on for this deployment
api_error500Our fault; safe to retry with the same idempotency key

An id that exists in another organization returns the same 404 as one that never existed.

Every response, error or not, carries a Rail-Request-Id header. Send us a Rail-Request-Id or X-Request-Id header of your own and we echo it back, as long as it's req_ plus 24 hex characters or a uuid; anything else, and we mint a new one instead. Quote this id to support and we can find the exact request.

Versioning

Every response carries X-Rail-Version with a date (currently 2026-08-21). Additive changes don't bump it: new fields, new endpoints and new enum values. So build parsers that ignore what they don't recognise. A breaking change becomes a new version, announced ahead of time. Existing keys keep the old behaviour.

Live and test mode

Every response carries X-Rail-Livemode (true or false), taken from the key that made the request.

The header only says which kind of key made the call. A rail_test_ key reads and writes the same rows as a live one. So a test key in staging still writes to your real program. If you want a production key in staging to fail loudly, assert on the header. See authentication for what a test key does and doesn't give you.