Flitch

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.

CodeStatusMeans
unauthorized401Missing, invalid, revoked, or expired token.
forbidden403Outside the credential's scopes or allowlist, or the API is not enabled for the account.
not_found404The resource does not exist.
invalid_request400Malformed body or a missing required parameter.
validation_failed422The write was understood but rejected, for example an unknown column.
idempotency_conflict409An Idempotency-Key reused with a different body, or a failed compare-and-set.
rate_limited429Too many requests in the current window.
upstream_error502A data source Flitch called failed.
internal_error500A 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: 60

X-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: 42

Wait 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.

On this page