Response envelope
/api/v1, and the boundary between them is the status code: 2xx carries { data, meta }, every non-2xx carries { error }. Nothing else is ever returned, so a client needs one parser and one switch on error.code.- Success
- data + meta
- Every refusal
- error: code, message, category
- Categories
- auth · rate_limit · validation · google · not_found · internal
- Content type
- application/json
- Body ceiling
- 1 MB in; larger is 413
4.1Success
docs/backend-api-contract.md §1.2, quoted as written
{
"data": <result>,
"meta": {
"api_id": "…",
"count": 12,
"limit": null,
"offset": 0,
"duration_ms": 143
}
}The two members
| Member | Shape | Meaning |
|---|---|---|
data | <result> | What the operation returns, and nothing around it: an array of record objects for a list or search, one object for a single record, { rows, headers } for a range, a write counter for POST/PATCH/DELETE. |
meta | object | The five fields §1.2 lists: api_id, count, limit, offset, duration_ms. |
What each meta field is stated to mean
| Field | Example in §1.2 | Documented meaning |
|---|---|---|
api_id | "…" | The API that answered — api_configurations.id, which is also the path segment you used. |
count | 12 | §1.3 defines it for the list operation: the rows returned. It is not documented as the total size of the tab, so do not use it as one. |
limit | null | A number or null, and §1.2's own example shows null alongside a non-zero count. §1.3 gives limit a documented default of all rows. The contract does not state more about what is echoed, so this page does not either. |
offset | 0 | The skip position of this page; 0 is the start, and page/per_page are the alternative way to arrive at the same offset. |
duration_ms | 143 | The engine's own measured duration — §4.8 makes it the same number the request-log row carries and the number the playground reports, so a slow call is slow in every view of it. |
4.2Error
docs/backend-api-contract.md §1.2, quoted as written
{
"error": {
"code": "record_not_found",
"message": "No record with id=42",
"category": "not_found"
}
}| Member | Type | What it carries |
|---|---|---|
code | string | The machine-readable reason, from the documented set on the Error reference page. Branch on this, not on the message. |
message | string | Human text for a log line or a support ticket. It is safe to show to your own users: §4.4 requires messages to never contain SQL, stack traces, tokens, keys, cell contents or another user's data — and the 500 row of §1.2 states the same for internal_error. |
category | enum | One of auth, rate_limit, validation, google, not_found, internal (§4.4 's ErrorCategory). It is the class of the failure, which helps group failures; use the specific code and operation to decide whether to retry. |
4.3Reading a response
docs/backend-api-contract.md §1.2 · §1.3 · §4.4
The worked pair below is a single-record read: the request plate is a real SnippetRequest, and the two bodies are the shapes above with data filled in the way §1.3 specifies for that operation.
The call
curl 'https://sheet.zaber.tech/api/v1/<api-id>/<record>' \ -H "Authorization: Bearer sz_live_…"
If it succeeds
{
"data": { "<header cell>": "<cell value>" },
"meta": {
"api_id": "<api-id>",
"count": <rows returned>,
"limit": <the page size you asked for, or null>,
"offset": <rows skipped, or 0>,
"duration_ms": <server milliseconds>
}
}If the value matches no row
{
"error": {
"code": "record_not_found",
"message": "No record with id=42",
"category": "not_found"
}
}The message is §1.2's own example. The row is not found and the response says so in the one shape every refusal uses.
- The contract uses two success boundaries. §1.2 defines the public error envelope as all non-2xx, while §4.6 defines a successful log row as status below 400. Those definitions differ for redirects; use the 2xx boundary for response handling and leave the log-filter discrepancy for the contract maintainers.
- Retry by code and operation, not category alone. Rate limits require waiting for
Retry-After; reauth requires consent, and inaccessible sheets require an access fix. Check the outcome of a failed write before repeating it — the contract does not promise idempotent retries. - There is no partial envelope. A response is either
data+metaorerror; the contract does not define a body that carries both.
4.4The dashboard API is a different envelope
docs/backend-api-contract.md §4.4, §2
Sheet Zaber has a second surface, /api/internal, used by the signed-in dashboard with a session cookie. Its success body is { data: … } — no meta — and §4.4 states plainly that the two are not interchangeable. These pages document /api/v1. The internal surface appears here in one place only: the Error reference page, because §4.4 is what fixes the category-to-status mapping.