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 · 03 Authentication

Authentication

/api/v1 has one credential: an API key in the Authorization header. There is no session, no OAuth token and no signed request. §1.1 of the contract defines the header, the five properties a key must have and the two statuses that refuse it.
Header
Authorization: Bearer …
Key format
sz_live_ + 32 hex
Missing or invalid
401
Scope or restriction violated
403
Query-parameter auth
not supported
Where keys are issued
/keys, after sign-in

3.1The header

docs/backend-api-contract.md §1.1

Exactly as §1.1 prints it
Authorization: Bearer sz_live_<32 hex>

A key is Bearer-style: whoever presents it is treated as its owner, so it belongs in a header on a TLS connection and nowhere else. The shape is a fixed prefix plus 32 hex characters :

Anatomy
sz_live_  +  32 hex characters
         ^              ^
         prefix         crypto.randomBytes(16).toString("hex") — 128 bits, docs/plan.md D10

3.2The five conditions a key must meet

docs/backend-api-contract.md §1.1 · refusal statuses from §1.1 and §1.2

Every request is checked against all five. A failure of the first three is an authentication problem (401); a failure of the last two is an authorisation problem (403).

ConditionStatus if it failsWhat it means, and the field behind it
unrevoked401api_keys.revoked_at is null. A revoked key is answered the same way as one that never existed, so a refusal is not a probe result (docs/security.md §3 item 2).
unexpired401api_keys.expires_at is null or in the future. Expiry is a field you set when creating the key.
owned by the API's user401The key's owner and the API's owner are the same account. The engine walks key → key owner → API owner equality, and there is no path that accepts a caller-supplied connection or token.
scope-compatible with the verb403read covers GET; write covers POST, PATCH, PUT and DELETE. A key's scopes is either ["read"] or ["read","write"].
within its API restriction403If allowed_api_ids is set, the requested API must be in that list; null means every API the same user owns.
  • The API itself must be enabled. A toggled-off API answers 403 api_disabled — the owner flips it back in the dashboard.
  • The verb must be allowed on the API. Each API carries an allowed_operations subset of the five verbs; anything outside it is 403 operation_not_allowed.
  • If the owner of the key is disabled, the key stops working. Disabling a user takes effect on the next request — logout for the session and 403 for the API (contract §2, admin row for setUserStatus).

3.3Scopes and per-key restrictions

docs/backend-api-contract.md §1.1 · §4.6 ApiKeyMeta

Two independent controls narrow what a key can do, and both are set at creation:

  • Scopes decide which verbs are reachable. A read-only integration gets read, and a POST/PATCH/PUT/DELETE call against it is a 403 key_scope_insufficient rather than a silently ignored write.
  • allowed_api_ids decides which endpoints are reachable. Give a partner one API and list only that id; leave it null for a key that should reach everything the account owns. A call outside the list is 403 key_restricted.

Scope is necessary but not sufficient: a write-scoped key still needs the API to be read_write, the verb in its allowed_operations, and — for PATCH, PUT and DELETE — a configured primary-key column. Those refusals are documented per operation on the Operations page.

3.4Key preview versus plaintext

docs/backend-api-contract.md §4.6, §4.8 · docs/security.md §2, §4

This is a documented security property, so it is stated here rather than buried in the dashboard: a plaintext API key is shown exactly once, at creation.

  • The database stores key_hash — the SHA-256 of the plaintext — and nothing else. Authentication hashes what you send and looks that hash up (§4.7 authenticateKey), so the platform cannot read your key back .
  • The only displayable form is keyPreview, documented as "sz_live_ab12…last4" — the first 10 plus the last 4 characters . A key list never carries the hash or the plaintext: both are projected out before serialization.
  • The plaintext exists only in the return value of createKey and regenerateKey (CreatedApiKey = ApiKeyMeta & { plaintext }, §4.6). Losing it means issuing a new key — there is no recovery, and that is the design.
  • Regenerating is revoke-plus-create, never an in-place mutation of a hash, so the history of the old key survives and a fresh plaintext comes back once.
  • Even the dashboard's own playground sends a key id, never a key string: it calls the engine in process with { kind: "keyId" }, so no plaintext enters the browser or a network log.

3.5An authenticated call, and a refusal

docs/backend-api-contract.md §1.1, §1.2

The request

The builders add the Authorization header themselves from the apiKey field, so the header is not something an example can forget.

cURL
curl 'https://sheet.zaber.tech/api/v1/<api-id>?limit=1' \
  -H "Authorization: Bearer sz_live_…"

No header at all

401 · application/json
{
  "error": {
    "code": "missing_key",
    "message": "<what failed>",
    "category": "auth"
  }
}

`code` names the reason — `missing_key` when the header is absent, `invalid_key` when it does not resolve.

Where a key comes from

Keys are issued in the dashboard, where you also set the name, the scopes, the API restriction and the expiry. The creation form returns the plaintext once; the list after that shows previews only.