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 space is on Free.
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.

The space's ceiling

There is a second limit above the per-credential one: a total for everything a space sends, across every key it holds and every person signed into it. A 429 from that limit carries X-RateLimit-Scope: team and names the space's total rather than the credential's.

It exists because the per-key number only makes each client polite. A space can hold any number of keys, and each person carries their own token, so without a ceiling the total reaching Flitch is unbounded no matter how modest each key looks.

If you see it, the fix is usually not a bigger number: it is one client polling far harder than the data changes. Reads of connection-backed data are cached, and a cache hit never reaches the source, so widening your polling interval often costs you nothing in freshness.

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 know. A breaking change ships as /api/v2, and /api/v1 keeps working for at least 12 months afterwards.

On this page