Operations
{api} is api_configurations.id, the header row is H, and data rows begin at H+1 — so the header row you designate is the row that names your columns, and everything under it is records.- Path prefix
https://sheet.zaber.tech/api/v1- Read verbs
- GET list · GET one · GET search · GET range
- Write verbs
- POST · PATCH · PUT · DELETE
- Write needs
- write scope + allowed_operations + read_write
- Bulk update/delete
- not in v1; batch create is disputed (see 5.5)
5.0How to read these plates
docs/backend-api-contract.md §1.2, §1.3
- Statuses. §1.3 names
201for Create; it does not print a response shape for Replace. The other success plates use illustrative 200 statuses — §1.2 defines all non-2xx as the error case, so a body withdataandmetais a success whatever the 2xx number is. - Failure lists. Each plate starts with the refusals that are specific to it, then the ones every call can meet (§1.1 authentication, §1.2 statuses). Nothing is listed that the contract does not define.
- Column names. These illustrative requests assume headers
id,email,status,created_at,note, withidas the primary key for record paths. Replace these with your headers. An unknown filter or sort column is 400; an unknown write-body key is422 header_mismatch. - Query encoding. The examples are serialized by
src/lib/snippets.tswithapplication/x-www-form-urlencoded, which is why a repeatedsortrepeats its key and a comma insidefieldsappears as%2C.
5.1List records
docs/backend-api-contract.md §1.3 · §1.2 envelope
Every row of the configured tab below the header row, as objects whose keys are the header cells. Filtering, sorting, paging and column selection all happen on the way to the answer, and an unknown column in a filter or a sort is a 400 — the tab is not queried by a language it does not have.
List records query parameters
| Parameter | Shape | Meaning |
|---|---|---|
limit | int ≥1, ≤1000 | Page size. Optional; the documented default is all rows. |
offset | int ≥0 | Rows to skip. Described as SheetDB-compatible. |
page + per_page | int | Alternative pagination, where offset = (page-1)*per_page. Rejected if mixed with offset. |
sort | column | -column | Ascending, or descending with a leading -. Repeatable: sort=a&sort=-b. |
order | column | An alias of sort, kept for SheetDB compatibility. |
fields | select | csv of columns | A comma-separated column subset, applied after filtering. |
q | text | Shortcut for a search across all columns — the same thing /search does. |
any column=value | exact match | Equality filter, repeatable and combined with AND. Value comparison is exact and case-sensitive. The operator suffixes __contains, __gt, __gte, __lt and __lte are supported. |
Success body (§1.2 + §1.3)
{
"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>
}
}Field names are the contract's; values are angle brackets so this cannot be read as a captured response.
Example call
Illustrative request built by src/lib/snippets.ts, the same builders the playground uses. Replace <api-id>, <record> and sz_live_… with your values and use your own sheet headers. This is not a live request or captured response.
curl 'https://sheet.zaber.tech/api/v1/<api-id>?status=open&sort=-created_at&fields=id%2Cemail%2Cstatus&limit=25' \ -H "Authorization: Bearer sz_live_…"
When it fails
- 400validation_failedA filter or sort names a column the header row does not have, or page/per_page is mixed with offset.
- 401missing_key · invalid_keyNo Authorization header, or a key that is revoked, expired, or owned by a different user than the API.
- 403key_scope_insufficient · key_restricted · api_disabled · operation_not_allowedThe key, its scope, its API restriction, the enabled state of the API or its allowed operations refuse the verb.
- 404api_not_foundNo API with that id — and the same answer for an API belonging to someone else, so the id space is not an oracle.
- 422tab_not_found · google_sheet_inaccessibleThe configured tab is not in the snapshot for that API, or the sheet cannot be read with the owner's Google grant.
- 429rate_limitedAn hourly or daily window is full. The response carries Retry-After in seconds.
- 500internal_errorAnything unexpected. The message never contains internals, and the request is still logged.
- 503google_reauth_required · google_quota_exceededReauth requires the owner's fresh consent; quota exhaustion instead requires waiting for Google's Retry-After. Check the code before retrying.
5.2Single record
docs/backend-api-contract.md §1.3 · Single record
One record, addressed by the value of the primary-key column you designated for that API. The path segment is that value — never a row number. If the API has no primary_key_column the call is refused with 403 operation_not_allowed, and the message tells you to set a primary key or use list plus a filter instead.
Single record path segment
| Segment | Shape | Meaning |
|---|---|---|
{record} | primary-key value | Matched case-sensitively against the stringified cell. Zero matches is 404, more than one is 409. |
Success body
{
"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>
}
}Example call
Illustrative request built by src/lib/snippets.ts, the same builders the playground uses. Replace <api-id>, <record> and sz_live_… with your values and use your own sheet headers. This is not a live request or captured response.
curl 'https://sheet.zaber.tech/api/v1/<api-id>/<record>' \ -H "Authorization: Bearer sz_live_…"
When it fails
- 401missing_key · invalid_keyNo Authorization header, or a key that is revoked, expired, or owned by a different user than the API.
- 403operation_not_allowedThe API has no primary_key_column, so a single record cannot be addressed at all.
- 403key_scope_insufficient · key_restricted · api_disabled · operation_not_allowedThe key, its scope, its API restriction, the enabled state of the API or its allowed operations refuse the verb.
- 404record_not_foundNo row matched that primary-key value.
- 404api_not_foundNo API with that id — and the same answer for an API belonging to someone else, so the id space is not an oracle.
- 409ambiguous_recordMore than one row carries that value, so the platform refuses rather than picking one.
- 422tab_not_found · google_sheet_inaccessibleThe configured tab is not in the snapshot for that API, or the sheet cannot be read with the owner's Google grant.
- 429rate_limitedAn hourly or daily window is full. The response carries Retry-After in seconds.
- 500internal_errorAnything unexpected. The message never contains internals, and the request is still logged.
- 503google_reauth_required · google_quota_exceededReauth requires the owner's fresh consent; quota exhaustion instead requires waiting for Google's Retry-After. Check the code before retrying.
5.3Search
docs/backend-api-contract.md §1.3 · Search
A case-insensitive substring match across all columns. It takes the same limit, offset, sort and fields parameters as the list call, and an empty q is a 400 rather than a match-everything query.
Search parameters
| Parameter | Shape | Meaning |
|---|---|---|
q | text, required | The substring to look for. Empty → 400 validation_failed. |
limit · offset · sort · fields | as in List records | The same paging, ordering and column-subset parameters, with the same meanings and bounds. |
Success body
{
"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>
}
}Example call
Illustrative request built by src/lib/snippets.ts, the same builders the playground uses. Replace <api-id>, <record> and sz_live_… with your values and use your own sheet headers. This is not a live request or captured response.
curl 'https://sheet.zaber.tech/api/v1/<api-id>/search?q=open&limit=10' \ -H "Authorization: Bearer sz_live_…"
When it fails
- 400validation_failedq was absent or empty — search never degrades into a full-table scan.
- 401missing_key · invalid_keyNo Authorization header, or a key that is revoked, expired, or owned by a different user than the API.
- 403key_scope_insufficient · key_restricted · api_disabled · operation_not_allowedThe key, its scope, its API restriction, the enabled state of the API or its allowed operations refuse the verb.
- 404api_not_foundNo API with that id — and the same answer for an API belonging to someone else, so the id space is not an oracle.
- 422tab_not_found · google_sheet_inaccessibleThe configured tab is not in the snapshot for that API, or the sheet cannot be read with the owner's Google grant.
- 429rate_limitedAn hourly or daily window is full. The response carries Retry-After in seconds.
- 500internal_errorAnything unexpected. The message never contains internals, and the request is still logged.
- 503google_reauth_required · google_quota_exceededReauth requires the owner's fresh consent; quota exhaustion instead requires waiting for Google's Retry-After. Check the code before retrying.
5.4Range
docs/backend-api-contract.md §1.3 · Range
The slice you asked for, as cells rather than as records: range/A1:D20, or range/Sheet1!A1:D20 when the sheet part matches the tab the API is configured on. Notation is checked by regex and against the tab's bounds, so a range outside the tab is a 400 and never a silently truncated answer.
Range path segment
| Segment | Shape | Meaning |
|---|---|---|
{A1notation} | A1:D20 | Sheet1!A1:D20 | Validated by regex plus a bounds check against the tab size. A sheet part that is not the configured tab is refused. |
Success body
{
"data": {
"rows": [
["<cell>", "<cell>"],
["<cell>", "<cell>"]
],
"headers": ["<header cell>", "<header cell>"]
},
"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>
}
}Example call
Illustrative request built by src/lib/snippets.ts, the same builders the playground uses. Replace <api-id>, <record> and sz_live_… with your values and use your own sheet headers. This is not a live request or captured response.
curl 'https://sheet.zaber.tech/api/v1/<api-id>/range/A1:D20' \ -H "Authorization: Bearer sz_live_…"
When it fails
- 400invalid_rangeNotation the regex rejects, a range outside the tab, or a sheet part that is not the configured tab.
- 401missing_key · invalid_keyNo Authorization header, or a key that is revoked, expired, or owned by a different user than the API.
- 403key_scope_insufficient · key_restricted · api_disabled · operation_not_allowedThe key, its scope, its API restriction, the enabled state of the API or its allowed operations refuse the verb.
- 404api_not_foundNo API with that id — and the same answer for an API belonging to someone else, so the id space is not an oracle.
- 422tab_not_found · google_sheet_inaccessibleThe configured tab is not in the snapshot for that API, or the sheet cannot be read with the owner's Google grant.
- 429rate_limitedAn hourly or daily window is full. The response carries Retry-After in seconds.
- 500internal_errorAnything unexpected. The message never contains internals, and the request is still logged.
- 503google_reauth_required · google_quota_exceededReauth requires the owner's fresh consent; quota exhaustion instead requires waiting for Google's Retry-After. Check the code before retrying.
5.5Create
docs/backend-api-contract.md §1.3 · Create
Append a record after the last non-empty row of the tab. Every key in the body must be a column the header row knows — an unknown key is a 422 header_mismatch whose message lists the offenders, while a missing key is fine and leaves the cell empty. Values are stringified for Sheets, and a key that your header row does not know is refused before anything is written.
Create request body
| Body | Shape | Meaning |
|---|---|---|
object | JSON object | Column names as keys. Unknown key → 422 header_mismatch; omitted keys are written as empty cells. |
array of objects | ≤500 per request | §1.3 documents the array form with a 500-record ceiling; see the note below before you rely on it, because docs/google-integration.md §4 documents v1 as one record per request. |
Create requirements
| Requirement | Where | Meaning |
|---|---|---|
write scope | §1.1 | The key must carry the write scope, which covers POST. |
POST allowed | §1.3 | POST must be in the API's allowed_operations. |
read_write API | §1.3 | The API's permissions must be read_write. |
no primary key needed | docs/plan.md D9 | Append does not resolve a row, so an API without a primary key can still accept POST. |
Success body
{
"data": {
"created": <records written>,
"records": [
{ "<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>
}
}The write itself goes through values.append with insertDataOption=INSERT_ROWS.
Example call
Illustrative request built by src/lib/snippets.ts, the same builders the playground uses. Replace <api-id>, <record> and sz_live_… with your values and use your own sheet headers. This is not a live request or captured response.
curl 'https://sheet.zaber.tech/api/v1/<api-id>' \
--request POST \
-H "Authorization: Bearer sz_live_…" \
-H "Content-Type: application/json" \
--data-raw '{"email":"ada@example.com","status":"open"}'When it fails
- 400invalid_jsonThe body is not parseable JSON.
- 401missing_key · invalid_keyNo Authorization header, or a key that is revoked, expired, or owned by a different user than the API.
- 403key_scope_insufficient · key_restricted · api_disabled · operation_not_allowedThe key, its scope, its API restriction, the enabled state of the API or its allowed operations refuse the verb.
- 404api_not_foundNo API with that id — and the same answer for an API belonging to someone else, so the id space is not an oracle.
- 413body_too_largeThe request body is over the 1 MB ceiling §1.2 documents.
- 422header_mismatchA body key is not in the header row; the message lists the offending keys.
- 422tab_not_found · google_sheet_inaccessibleThe configured tab is not in the snapshot for that API, or the sheet cannot be read with the owner's Google grant.
- 429rate_limitedAn hourly or daily window is full. The response carries Retry-After in seconds.
- 500internal_errorAnything unexpected. The message never contains internals, and the request is still logged.
- 503google_reauth_required · google_quota_exceededReauth requires the owner's fresh consent; quota exhaustion instead requires waiting for Google's Retry-After. Check the code before retrying.
5.6Update (merge)
docs/backend-api-contract.md §1.3 · Update (merge)
A partial object: only the columns you send are written, and the row is resolved through the primary-key value at request time. The write goes through values.update per contiguous span, and data.updated is 1.
Update request body
| Body | Shape | Meaning |
|---|---|---|
partial object | JSON object | Any subset of the header row's columns. Keys the header row does not know are a 422 header_mismatch; columns you leave out keep their value. |
Success body
{
"data": { "updated": 1 },
"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>
}
}Example call
Illustrative request built by src/lib/snippets.ts, the same builders the playground uses. Replace <api-id>, <record> and sz_live_… with your values and use your own sheet headers. This is not a live request or captured response.
curl 'https://sheet.zaber.tech/api/v1/<api-id>/<record>' \
--request PATCH \
-H "Authorization: Bearer sz_live_…" \
-H "Content-Type: application/json" \
--data-raw '{"status":"shipped"}'When it fails
- 400invalid_jsonThe body is not parseable JSON.
- 401missing_key · invalid_keyNo Authorization header, or a key that is revoked, expired, or owned by a different user than the API.
- 403write_requires_primary_key · operation_not_allowedA write verb against an API whose primary_key_column is unset, or whose allowed_operations omit PATCH.
- 403key_scope_insufficient · key_restricted · api_disabled · operation_not_allowedThe key, its scope, its API restriction, the enabled state of the API or its allowed operations refuse the verb.
- 404record_not_foundThe primary-key value matched no row, so there is nothing to merge into.
- 404api_not_foundNo API with that id — and the same answer for an API belonging to someone else, so the id space is not an oracle.
- 409ambiguous_recordThe primary-key value matched more than one row: §1.5 promises ambiguity always 409s rather than writing two rows.
- 413body_too_largeThe request body is over 1 MB.
- 422header_mismatchA body key is not in the header row.
- 422tab_not_found · google_sheet_inaccessibleThe configured tab is not in the snapshot for that API, or the sheet cannot be read with the owner's Google grant.
- 429rate_limitedAn hourly or daily window is full. The response carries Retry-After in seconds.
- 500internal_errorAnything unexpected. The message never contains internals, and the request is still logged.
- 503google_reauth_required · google_quota_exceededReauth requires the owner's fresh consent; quota exhaustion instead requires waiting for Google's Retry-After. Check the code before retrying.
5.7Replace
docs/backend-api-contract.md §1.3 · Replace
A full object over one row. §1.3 states the divergence from PATCH in one sentence: every header column except the primary key is optional-but-overwritten, and a column you omit is cleared. If you meant to merge, send PATCH.
Replace request body
| Body | Shape | Meaning |
|---|---|---|
full object | JSON object | Every header column you want to keep. Omitted columns are overwritten — this is the documented divergence from PATCH, not an accident of ordering. |
Example call
Illustrative request built by src/lib/snippets.ts, the same builders the playground uses. Replace <api-id>, <record> and sz_live_… with your values and use your own sheet headers. This is not a live request or captured response.
curl 'https://sheet.zaber.tech/api/v1/<api-id>/<record>' \
--request PUT \
-H "Authorization: Bearer sz_live_…" \
-H "Content-Type: application/json" \
--data-raw '{"email":"ada@example.com","status":"shipped","note":""}'When it fails
- 400invalid_jsonThe body is not parseable JSON.
- 401missing_key · invalid_keyNo Authorization header, or a key that is revoked, expired, or owned by a different user than the API.
- 403write_requires_primary_key · operation_not_allowedA write verb against an API whose primary_key_column is unset, or whose allowed_operations omit PUT.
- 403key_scope_insufficient · key_restricted · api_disabled · operation_not_allowedThe key, its scope, its API restriction, the enabled state of the API or its allowed operations refuse the verb.
- 404record_not_foundThe primary-key value matched no row.
- 404api_not_foundNo API with that id — and the same answer for an API belonging to someone else, so the id space is not an oracle.
- 409ambiguous_recordThe primary-key value matched more than one row.
- 413body_too_largeThe request body is over 1 MB.
- 422header_mismatchA body key is not in the header row.
- 422tab_not_found · google_sheet_inaccessibleThe configured tab is not in the snapshot for that API, or the sheet cannot be read with the owner's Google grant.
- 429rate_limitedAn hourly or daily window is full. The response carries Retry-After in seconds.
- 500internal_errorAnything unexpected. The message never contains internals, and the request is still logged.
- 503google_reauth_required · google_quota_exceededReauth requires the owner's fresh consent; quota exhaustion instead requires waiting for Google's Retry-After. Check the code before retrying.
5.8Delete
docs/backend-api-contract.md §1.3 · Delete
One row, resolved by its primary-key value, removed with spreadsheets.batchUpdate → deleteDimension for that single row. A body on a DELETE is rejected with 400 — there is no conditional-delete form in v1.
Delete path segment
| Segment | Shape | Meaning |
|---|---|---|
{record} | primary-key value | The same resolution the read and merge verbs use: 0 matches → 404, more than 1 → 409. |
Success body
{
"data": { "deleted": 1 },
"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>
}
}Example call
Illustrative request built by src/lib/snippets.ts, the same builders the playground uses. Replace <api-id>, <record> and sz_live_… with your values and use your own sheet headers. This is not a live request or captured response.
curl 'https://sheet.zaber.tech/api/v1/<api-id>/<record>' \ --request DELETE \ -H "Authorization: Bearer sz_live_…"
When it fails
- 400validation_failedA body was sent with the DELETE — §1.3 rejects body-bearing deletes outright.
- 401missing_key · invalid_keyNo Authorization header, or a key that is revoked, expired, or owned by a different user than the API.
- 403write_requires_primary_key · operation_not_allowedA delete against an API whose primary_key_column is unset, or whose allowed_operations omit DELETE.
- 403key_scope_insufficient · key_restricted · api_disabled · operation_not_allowedThe key, its scope, its API restriction, the enabled state of the API or its allowed operations refuse the verb.
- 404record_not_foundThe primary-key value matched no row.
- 404api_not_foundNo API with that id — and the same answer for an API belonging to someone else, so the id space is not an oracle.
- 409ambiguous_recordThe primary-key value matched more than one row, so no row is deleted.
- 422tab_not_found · google_sheet_inaccessibleThe configured tab is not in the snapshot for that API, or the sheet cannot be read with the owner's Google grant.
- 429rate_limitedAn hourly or daily window is full. The response carries Retry-After in seconds.
- 500internal_errorAnything unexpected. The message never contains internals, and the request is still logged.
- 503google_reauth_required · google_quota_exceededReauth requires the owner's fresh consent; quota exhaustion instead requires waiting for Google's Retry-After. Check the code before retrying.
5.9Not in v1
docs/backend-api-contract.md §1.3 · Limits / batch, §4.10
The contract closes the section with a question it answers no: whether DELETE or PATCH accept a ?where[column]=value bulk form. No — v1 is single-record only, which the contract explains as matching spec §7 and “avoiding destructive bulk surprises”.
- Bulk update and bulk delete. Not offered; each PATCH, PUT or DELETE addresses one primary-key value. A body on a DELETE is a 400.
- Batch create. §1.3 and docs/google-integration.md §4 disagree about a POST array — see the note in section 5.5. Until that is reconciled, write one record per request.
- Query-string authentication. Not supported, so a key never has to appear in a URL, a log line or a referrer.
- Anything §4.10 defers. The deferred and blocked surfaces are listed on the Overview page, and none of them is advertised as a capability of the data API here.