Errors and limits
Error codes, rate limits, and replaying writes safely.
Error shape
Failures return one shape, with a stable machine-readable code:
{
"error": {
"code": "forbidden",
"message": "This credential cannot access that resource.",
"details": {}
}
}details is present only where it adds something, such as the offending column on a validation failure. Branch on code, never on message: messages are written for humans and may be reworded.
| Code | Status | Means |
|---|---|---|
unauthorized | 401 | Missing, invalid, revoked, or expired token. |
forbidden | 403 | Outside the credential's scopes or allowlist, or the API is not enabled for the account. |
not_found | 404 | The resource does not exist. |
invalid_request | 400 | Malformed body or a missing required parameter. |
validation_failed | 422 | The write was understood but rejected, for example an unknown column. |
idempotency_conflict | 409 | An Idempotency-Key reused with a different body, or a failed compare-and-set. |
rate_limited | 429 | Too many requests in the current window. |
upstream_error | 502 | A data source Flitch called failed. |
internal_error | 500 | A fault on our side. |
A 403 on an unlisted resource does not disclose whether that resource exists. An id outside a key's allowlist and an id that was never real are answered identically, on purpose.
Every endpoint answers this shape, including the two reads that pass through to a data source. Their successful responses carry "success": true alongside the data, which is a property of the payload rather than a second error convention.
Rate limits
Requests are counted per credential in a fixed 60 second window. The limit is chosen when the key is created (60, 600, or 3,000 per minute, defaulting to 600) and shown on each key in Settings → API Keys. Session tokens carry a fixed 600 per user.
Every response carries the current state, so a client can slow down before it is refused rather than after:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 587
X-RateLimit-Reset: 60X-RateLimit-Reset is the seconds remaining in the window. Exceeding the limit returns 429 with the same headers plus Retry-After:
Retry-After: 42
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 42Wait Retry-After seconds before retrying. Rejected requests still count toward the window, so hammering a throttled endpoint keeps it closed rather than resetting it.
Responses also carry a Server-Timing header reporting how long authentication took, which shows up in the browser's network panel without any instrumentation of your own.
Reads of connection-backed data are cached, and a cache hit does not reach the upstream source. Prefer letting the cache serve repeat queries over building your own polling loop with a tighter interval than the data actually changes.
Idempotent writes
Any write accepts an Idempotency-Key header. It is how a client that queues writes offline can drain the queue on reconnect without risking duplicates.
Idempotency-Key: 5f2c1e90-job-1042-complete- Replaying a key within 24 hours returns the original response unchanged and makes no second change. The replay carries
Idempotent-Replay: true. - Reusing a key with a different body returns
409 idempotency_conflict. That is a client bug worth surfacing rather than silently serving the wrong cached answer. - Keys are scoped per credential, so two clients cannot collide on the same string.
- After 24 hours a key is forgotten and would be treated as a new request.
Generate one key per logical operation, not per attempt. A retry of "mark job 1042 complete" must send the same key the first attempt did, or it is not a retry.
Without the header, writes are unguarded. The header is how a client opts in.
Versioning
Endpoints live under /api/v1. Additive changes, such as a new field in a response, ship without a version bump, so parse responses tolerantly and ignore fields you do not recognise. A breaking change ships as /api/v2, and /api/v1 keeps working for at least 12 months afterwards.