Skip to content
Open beta — everything’s free right now, and your rate is locked when it ends.

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

Tabla is the database we build these on.

A no-code database with real Postgres underneath: every feature on every plan, a million records per database, and your whole database back out in one file, any day.

More guides