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:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The bucket size (25) |
X-RateLimit-Remaining | Requests left right now |
X-RateLimit-Reset | Unix 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:
| Type | Status | When |
|---|---|---|
authentication_error | 401 | Missing, malformed, unknown or revoked key |
permission_error | 403 | The key lacks the scope; the message names the one it needed |
invalid_request_error | 400, 422 | A parameter or body that doesn't parse or validate; a reused idempotency key |
not_found_error | 404 | No such object in your organization (resource_missing) |
conflict_error | 409 | The write contradicts current state |
rate_limit_error | 429 | Over 25 requests per second |
plan_error | 402 | The account's plan has lapsed; the key is paused, nothing is deleted |
not_implemented_error | 501 | A documented capability not switched on for this deployment |
api_error | 500 | Our 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.