API errors and limits: one envelope, every number
Every failure is {“error”:{“code”,“message”}} with an honest status — real 400, 401, 404, 409 and 429 responses, every enforced limit in one table, and the stability promise in writing.
Updated August 2026
Every failure answers with the same envelope: an error object carrying a
machine code and a human message, under a status that agrees with the
code. Branch on the status and the code; the message names the offending
field or record where the engine knows it, but its wording is for people and
is not part of the interface.
The envelope, five real ways
Each of these was produced against a live Tabla, not composed:
400 bad_request — a malformed body, an unknown field, a value a field
can't hold, a ceiling reached:
{"error":{"code":"bad_request","message":"Operator \"contains\" is not allowed on field \"Price\" (number). Allowed: is, is_not, greater, greater_or_equal, less, less_or_equal, empty, not_empty."}}
401 unauthorized — no key, or one that doesn't resolve:
{"error":{"code":"unauthorized","message":"Missing or invalid API key. Expected: Bearer <id>.<secret>."}}
404 not_found — the id doesn't exist, or it belongs to a database
your key's account isn't a member of. Both read identically, so probing ids
tells an outsider nothing:
{"error":{"code":"not_found","message":"Record rec_000000000000 not found."}}
409 conflict — the one documented exception to the flat envelope. A
PATCH carrying a stale if_updated_at refuses, and merges into the error a
records array holding each contested record as it is now, so a stale
client can heal without a second request:
{"error": {"code": "conflict",
"message": "1 record changed since it was loaded.",
"records": [{"id": "rec_ugg23f5f7uru",
"updated_at": "2026-08-09T23:06:15.345Z",
"fields": {"Name": "ORD-1056", "Status": "Preparing", "Total": 43}}]}}
(Trimmed; the records come back whole.)
429 rate_limited — over a cap, with the wait in a header:
HTTP/1.1 429 Too Many Requests
retry-after: 3
x-ratelimit-limit: 600
x-ratelimit-remaining: 0
{"error":{"code":"rate_limited","message":"Too many requests. Retry in about 3s."}}
Retry-After is whole seconds until the oldest counted request slides out of
the window — sleep that long, retry, and the retry fits. Limits are sliding
windows, so traffic that backs off resumes gradually, not in a thundering
top-of-the-minute herd. A 500 (internal) always says only "Something went
wrong."; the detail is logged server-side, never returned.
You don't have to wait for the 429 to see it coming. Every response to a request
carrying an API key — successes included, not just refusals — has
X-RateLimit-Limit (600, that key's budget for the minute) and
X-RateLimit-Remaining. Watch the second one and ease off before you're refused:
x-ratelimit-limit: 600
x-ratelimit-remaining: 587
Only the per-key budget is published. The address-keyed shields below aren't — they're not a promise to anyone, and a ceiling you share with everyone behind your office's address isn't a number you could act on.
Every enforced limit
| Limit | Value |
|---|---|
| Records per write batch | 100 — all-or-nothing; over it: Batches are capped at 100 records; got 101. |
page_size on a read |
1 to 1,000, default 100 |
| Requests per API key | 600 a minute, charged once the secret is verified |
| Requests per address, no credential | 60 a minute |
| Requests per address, with a bearer | 3,000 a minute — a shield before the key check, not the published cap |
| Attachment uploads | 60 a minute per account — refused as a 400 with the wait in its message |
| One attachment file | 100 MB |
| Attachment storage per account | 100 GB — across all its databases |
| Import file / rows per run (Excel) | 20 MB / 50,000 rows |
| Import, CSV or TSV | no limit (streamed from the browser) |
| Records per database | 1,000,000 |
| Records per account | 2,000,000 — across all its databases |
| Databases per account | 10 |
| Tables per database | 200 |
| Tables per account | 300 — across all its databases |
| Fields per table | 500 |
The last five are structural sanity caps, set far above legitimate use; no meter shows how close you are. Reaching any ceiling is a 400 naming it. Rate-limit counters live in the running application's memory and reset if it restarts — honest abuse limiting, not an accounting meter. Deleted records sit in the trash for 7 days before purge.
The stability promise
As /docs/api itself puts it: ids are opaque and permanent —
db_, tbl_, fld_, viw_, rec_, att_, whk_ and twelve characters —
and a rename changes a name, never an id, so anything automated should
reference tables and fields by id and ask for key_by=id on reads. The path
carries no version number. Response objects may grow new keys over time: a
client must ignore keys it does not recognize and must not depend on key
order. Removing a key, changing a value's type, or repurposing an error code
counts as a break. Shipped changes are listed newest first at
/changelog.
In practice: branch on codes (the shapes above
show where each arises), honor Retry-After (keys covers
the per-key budget), and key long-lived integrations by id
(names and ids).