Sheet Zaber · Reference

/api/v1 documentation

The contract in docs/backend-api-contract.md §1, rendered from the same snippet builders the playground uses. Private platform, public documentation — no signup on these pages.

Docs · 05 Operations

Operations

Eight operations against one path prefix. §1.3 fixes the notation used below: {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 201 for 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 with data and meta is 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, with id as the primary key for record paths. Replace these with your headers. An unknown filter or sort column is 400; an unknown write-body key is 422 header_mismatch.
  • Query encoding. The examples are serialized by src/lib/snippets.ts with application/x-www-form-urlencoded, which is why a repeated sort repeats its key and a comma inside fields appears 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.

GET/api/v1/{api}read scope
Success200data is an array of record objects; meta.count is the rows returned

List records query parameters

ParameterShapeMeaning
limitint ≥1, ≤1000Page size. Optional; the documented default is all rows.
offsetint ≥0Rows to skip. Described as SheetDB-compatible.
page + per_pageintAlternative pagination, where offset = (page-1)*per_page. Rejected if mixed with offset.
sortcolumn | -columnAscending, or descending with a leading -. Repeatable: sort=a&sort=-b.
ordercolumnAn alias of sort, kept for SheetDB compatibility.
fields | selectcsv of columnsA comma-separated column subset, applied after filtering.
qtextShortcut for a search across all columns — the same thing /search does.
any column=valueexact matchEquality 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)

application/json
{
  "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
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.

GET/api/v1/{api}/{record}read scope
Success200data is one record object

Single record path segment

SegmentShapeMeaning
{record}primary-key valueMatched case-sensitively against the stringified cell. Zero matches is 404, more than one is 409.

Success body

application/json
{
  "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
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.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.

GET/api/v1/{api}/range/{A1notation}read scope
Success200data.rows is the raw 2-D slice, data.headers the header cells

Range path segment

SegmentShapeMeaning
{A1notation}A1:D20 | Sheet1!A1:D20Validated by regex plus a bounds check against the tab size. A sheet part that is not the configured tab is refused.

Success body

application/json
{
  "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
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.

POST/api/v1/{api}write scope
Success201data.created is the count, data.records the echoed objects

Create request body

BodyShapeMeaning
objectJSON objectColumn 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

RequirementWhereMeaning
write scope§1.1The key must carry the write scope, which covers POST.
POST allowed§1.3POST must be in the API's allowed_operations.
read_write API§1.3The API's permissions must be read_write.
no primary key neededdocs/plan.md D9Append does not resolve a row, so an API without a primary key can still accept POST.

Success body

application/json
{
  "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
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.

PATCH/api/v1/{api}/{record}write scope
Success200data.updated is 1

Update request body

BodyShapeMeaning
partial objectJSON objectAny 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

application/json
{
  "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
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.

PUT/api/v1/{api}/{record}write scope
Success200the whole row is written — see the note on what §1.3 does not print

Replace request body

BodyShapeMeaning
full objectJSON objectEvery 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
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/api/v1/{api}/{record}write scope
Success200data.deleted is 1

Delete path segment

SegmentShapeMeaning
{record}primary-key valueThe same resolution the read and merge verbs use: 0 matches → 404, more than 1 → 409.

Success body

application/json
{
  "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
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.