Storage resources
Owner-declared KV, SQLite and durable file resources.
POST /v1/sites/{sitename}/storage/files/{name}/download-link
Mint a ten-minute scoped full-byte download link (owner only)
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| Idempotency-Key | header | string | Opaque key for a create request. For 24 hours, the same authenticated account, POST path and query, key and exact request body replay the original handler status and body, including errors, without executing it again. A different body under that key returns 409 idempotency_key_reused. Concurrent identical requests wait for the first response. Stored only in this server’s memory; a restart clears it. Authentication and rate limiting still apply to replays. A replay does not extend a minted link’s expiry; use a new key to request a new link. Storage row creates support owner credentials and signed-in visitors; anonymous writes ignore this header. Storage replay also checks current resource permissions and is scoped to the resolved site. |
Request body:
| Name | Type | Required | Description |
|---|---|---|---|
| path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | {url,expires_at}; bearer URL remains private and is rechecked when used |
| 409 | Idempotency-Key reused with a different body (idempotency_key_reused). |
GET /v1/sites/{sitename}/storage/files/{name}/objects
Checked against the read value. Visitors get mine per item; the owner gets visitor_id. Own lists use the writer/path index with LIMIT before opening files. Temporary dot objects are excluded. Exact legacy prefixes and cursors remain accepted; cursors follow scanned metadata even if files are missing.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| prefix | query | string | ||
| after | query | string | ||
| limit | query | integer |
Responses:
| Status | Description |
|---|---|
| 200 | items: [{path,bytes,content_type,mine|visitor_id}], next_after cursor |
GET /v1/sites/{sitename}/storage/files/{name}/objects/{path}
Own reads return only the signed-in visitor’s file; other paths return 404. The path may contain slash-separated segments. Only validated raster bytes render inline; other types are attachments with nosniff.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| path | path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | Raw file bytes |
| 404 | file_not_found |
PUT /v1/sites/{sitename}/storage/files/{name}/objects/{path}
The server decides add (a new path, checked against add, published without replacing) or edit (an existing path, checked against edit; own edits only by the author, who stays the author). A caller who may add but not edit gets 409 file_exists. The owner’s tools can always replace. File growth beyond the separate website allowance returns site_full with photo-shrinking tips. Empty files count toward SITE_STORAGE_FILES_MAX_OBJECTS (1000 per bucket by default); bucket_full is 507. New upload paths are normalized to NFC; reads and deletes accept exact legacy paths. Paths allow at most 16 segments.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| path | path | string | yes |
Request body: application/octet-stream
Responses:
| Status | Description |
|---|---|
| 200 | {path,bytes,content_type} |
| 507 | site_full |
DELETE /v1/sites/{sitename}/storage/files/{name}/objects/{path}
Checked against delete; own deletes only the author’s file (404 otherwise); a caller who may only add gets 403 add_only.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| path | path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | deleted: true |
GET /v1/sites/{sitename}/storage/kv/{name}/keys
Own lists return only the signed-in visitor’s keys. Visitors get mine true/false per item; the owner’s tools and the owner signed in on the site get visitor_id.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| prefix | query | string | ||
| after | query | string | ||
| limit | query | integer |
Responses:
| Status | Description |
|---|---|
| 200 | items: [{key,value,mine|visitor_id}], next_after cursor |
| 403 | forbidden or site_locked |
GET /v1/sites/{sitename}/storage/kv/{name}/keys/{key}
Own reads return only the signed-in visitor’s key; other keys return 404. Visitors get mine; the owner gets visitor_id.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| key | path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | {key,value} |
| 404 | key_not_found or resource_not_found |
PUT /v1/sites/{sitename}/storage/kv/{name}/keys/{key}
The server decides add (the key does not exist; checked against add, created with ON CONFLICT DO NOTHING) or edit (it exists; checked against edit, own edits carry the author check in the UPDATE and keep the original author). A caller who may add but not edit gets 409 key_exists on an existing key. The owner’s tools can always replace. New keys are normalized to NFC; exact legacy names remain readable and deletable. Visitor writes use the site’s own origin and X-SH-CSRF header; anonymous writes require write=anyone. Visitor writes are limited per site/IP and per site/visitor by RATE_LIMIT_STORAGE_IP and RATE_LIMIT_STORAGE_VISITOR (429); reads consume no write tokens. Writes wait SITE_STORAGE_WRITE_LOCK_WAIT_MS (default 2000) for the deploy lock before 503 storage_busy with Retry-After.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| key | path | string | yes |
Request body:
| Name | Type | Required | Description |
|---|---|---|---|
| value | any | yes |
Responses:
| Status | Description |
|---|---|
| 200 | {key,value} |
| 507 | site_full |
DELETE /v1/sites/{sitename}/storage/kv/{name}/keys/{key}
Checked against delete. Own deletes carry visitor_id = caller in the statement and answer 404 for another person’s key; a caller who may only add gets 403 add_only.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| key | path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | deleted: true |
GET /v1/sites/{sitename}/storage/people
Owner only: the owner’s key or connector token, or the owner signed in on the site’s own address (SH.me().site_owner; SH.storage.people() on the page). Lists only this site’s people who have saved data here (a SQLite row, a KV key, or a stored file) or are named viewers — never everyone who has merely signed in (the privacy rule). Each entry’s visitor_id, when present, is a usable for target; a named viewer who has not signed in yet is listed by email only (visitor_id omitted, signed_in false), not yet usable.
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | people: [{visitor_id?, email?, named_viewer, has_data, signed_in}] |
| 403 | forbidden (not the owner) |
GET /v1/sites/{sitename}/storage/resources
On Simple Host each resource carries preset (public, inbox, wall, comments, records, personal, board, private, custom with based_on, or legacy), access {read,add,edit,delete}, tables (SQLite, per-table preset and access), approve_first (SQLite), and the older read, write and write_mode whenever the matrix can be expressed in them. legacy marks a resource saved before presets that keeps its old rules until the owner saves a policy on it.
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | resources: [{name,kind,preset,access:{read,add,edit,delete},tables,read,write,write_mode,site_passcode}] |
| 403 | forbidden |
PUT /v1/sites/{sitename}/storage/resources/{name}
Kind is immutable. Simple Host: set preset (public, inbox, wall, comments, records, personal, board, private; default private) with optional single-action overrides read, add, edit, delete, or preset custom with all four. Each action takes nobody, owner, own, signed-in or anyone; add also takes checked (anyone who passes the Cloudflare Turnstile spam check: the comments preset, read anyone / add checked / edit owner / delete owner). The server refuses (400 invalid_access, with rule) edit or delete anyone (anonymous_change), add own (add_own), own anywhere with add anyone or checked (own_needs_sign_in), checked on read, edit or delete (checked_add_only), and edit or delete wider than read (change_wider_than_read). checked is refused on files resources, and with 400 spam_check_unavailable on a server without TURNSTILE_SITE_KEY and a secret. approve_first (SQLite, needs a checked add) holds posts added through the check: the server adds approved_at TEXT to those tables (rows already there count as approved), visitors see and reach only rows with approved_at set, and the owner approves with UPDATE … SET approved_at = datetime(‘now’). Omit approve_first to keep it. SQLite: the preset is the database default; tables sets a preset per table (omit to keep the current table presets, {} to clear). Tables whose preset uses own or lets visitors add get the server-owned visitor_id TEXT column and index. The older read, write and write_mode are still accepted and translated (write anyone with full is refused as anonymous_change; omitting write_mode on update keeps the mode); do not mix them with preset fields. Switching a legacy SQLite database pages wrote with SQL to a preset that uses own needs empty tables (visitor_id_required). Owner here means the owner’s tools and the owner signed in on the site’s own address through visitor sign-in; nobody means the owner’s tools only.
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes |
Request body:
| Name | Type | Required | Description |
|---|---|---|---|
| add | string, one of nobody, owner, signed-in, checked, anyone |
||
| approve_first | boolean | ||
| delete | string, one of nobody, owner, own, signed-in |
||
| edit | string, one of nobody, owner, own, signed-in |
||
| kind | string, one of kv, sqlite, files |
yes | |
| preset | string, one of public, inbox, wall, comments, records, personal, board, private, custom |
||
| read | string, one of nobody, owner, own, signed-in, anyone |
||
| site_passcode | string, one of inherit, False |
||
| tables | object | ||
| write | string, one of anyone, signed-in, owner |
older field | |
| write_mode | string, one of full, add |
older field |
Responses:
| Status | Description |
|---|---|
| 200 | Existing resource policy changed; the resource as listed |
| 201 | Resource created; the resource as listed |
| 400 | invalid_access (with rule), invalid_resource, visitor_id_required |
| 403 | forbidden |
| 409 | resource_kind_conflict, or tables_present (older fields on a database with per-table presets) |
DELETE /v1/sites/{sitename}/storage/resources/{name}
Permanently delete a resource and its data (owner only)
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | deleted: true |
| 403 | forbidden |
POST /v1/sites/{sitename}/storage/sqlite/{name}/execute
Owner only on any database with a preset; visitors receive 403 fixed_routes_required. A legacy full-mode database saved before presets keeps visitor SQL under its write policy. A statement that reads rows also needs read permission. Visitor writes require same-origin CSRF header. All SQLite executions share SITE_STORAGE_SQL_CONCURRENCY (0 selects CPU count); SITE_STORAGE_ACQUIRE_WAIT_MS (default 250) bounds queue waiting before 503 with Retry-After. Owner operations retain SITE_STORAGE_OWNER_TIMEOUT_MS (default 5000).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes |
Request body:
| Name | Type | Required | Description |
|---|---|---|---|
| params | array of any | Positional JSON scalar values | |
| sql | string | yes | One SQLite statement; use bound question-mark parameters. |
Responses:
| Status | Description |
|---|---|
| 200 | {changes,last_insert_id} |
| 400 | Every SQL refusal says what happened and what to do, under a stable code (full list on the StorageSQL schema): sql_request, sql_params, sql_syntax, sql_multiple_statements, sql_param_count, sql_not_available, sql_use_query, sql_use_execute, sql_use_schema, sql_no_such_table, sql_no_such_column, sql_no_such_function, sql_not_null_failed, sql_check_failed, sql_foreign_key_failed, sql_constraint_failed, sql_timeout, sql_value_too_large, sql_error |
| 403 | fixed_routes_required, or sql_visitor_rule (a legacy visitor statement reads rows the read rule does not allow; the message names the rule) |
| 409 | sql_unique_failed: a row with that unique value exists; the message names the column |
| 507 | site_full or sqlite_full |
POST /v1/sites/{sitename}/storage/sqlite/{name}/query
Owner only on any database with a preset; visitors receive 403 fixed_routes_required and use the table rows routes. A legacy full-mode database saved before presets keeps visitor SQL under its read policy until the owner sets a preset. The SQLite engine authorizer enforces read-only access. Results are bounded, with columns and rows. All SQLite executions share SITE_STORAGE_SQL_CONCURRENCY (0 selects CPU count); SITE_STORAGE_ACQUIRE_WAIT_MS (default 250) bounds queue waiting before 503 with Retry-After. Owner operations retain SITE_STORAGE_OWNER_TIMEOUT_MS (default 5000).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes |
Request body:
| Name | Type | Required | Description |
|---|---|---|---|
| params | array of any | Positional JSON scalar values | |
| sql | string | yes | One SQLite statement; use bound question-mark parameters. |
Responses:
| Status | Description |
|---|---|
| 200 | columns: […], rows: [[…]] |
| 400 | Every SQL refusal says what happened and what to do, under a stable code (full list on the StorageSQL schema): sql_request, sql_params, sql_syntax, sql_multiple_statements, sql_param_count, sql_not_available, sql_use_query, sql_use_execute, sql_use_schema, sql_no_such_table, sql_no_such_column, sql_no_such_function, sql_not_null_failed, sql_check_failed, sql_foreign_key_failed, sql_constraint_failed, sql_timeout, sql_value_too_large, sql_error, or result_too_large |
| 403 | fixed_routes_required (visitor on a preset database) |
POST /v1/sites/{sitename}/storage/sqlite/{name}/schema
New tables whose preset uses own or lets visitors add (every table of a legacy own database) receive visitor_id TEXT and its index; changes removing the column from such a table roll back. All SQLite executions share SITE_STORAGE_SQL_CONCURRENCY (0 selects CPU count); SITE_STORAGE_ACQUIRE_WAIT_MS (default 250) bounds queue waiting before 503 with Retry-After. Owner operations retain SITE_STORAGE_OWNER_TIMEOUT_MS (default 5000).
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes |
Request body:
| Name | Type | Required | Description |
|---|---|---|---|
| params | array of any | Positional JSON scalar values | |
| sql | string | yes | One SQLite statement; use bound question-mark parameters. |
Responses:
| Status | Description |
|---|---|
| 200 | changes: integer |
| 400 | Every SQL refusal says what happened and what to do, under a stable code (full list on the StorageSQL schema): sql_request, sql_params, sql_syntax, sql_multiple_statements, sql_param_count, sql_not_available, sql_use_query, sql_use_execute, sql_use_schema, sql_no_such_table, sql_no_such_column, sql_no_such_function, sql_not_null_failed, sql_check_failed, sql_foreign_key_failed, sql_constraint_failed, sql_timeout, sql_value_too_large, sql_error, visitor_id_required, or server_column (a CREATE TRIGGER/VIEW or ALTER naming written_by; SIM-358, 2026-11-06, a name match on the statement text, not a parser – a positional INSERT elsewhere naming no column is unaffected, by design) |
| 409 | table_preset_rename |
GET /v1/sites/{sitename}/storage/sqlite/{name}/tables/{table}/rows
Checked against the table’s read value (its own preset, or the database’s). Equality filters where.{column}=value (at most three, real columns only) are bound values. Visitors see their own visitor_id and null for other people’s, plus mine per row; visitor_id cannot be filtered or ordered by visitors. Own reads use WHERE visitor_id = ? with the stable site-session identity. Table and order columns are checked against real schema; values are bound. Default order is an unshadowed rowid alias ascending; ordinary rowid tables only. Cursor includes order value and rowid, bound on the next query with matching order/desc. Owner sees all. No visitor SQL accepted. HEAD is a read. Visitor tables are limited to 100 columns and the complete response to SITE_STORAGE_SQL_RESULT_MAX_BYTES; visitor queries time out after one second. Reads do not lock writes or deploys.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| table | path | string | yes | |
| order | query | string | ||
| desc | query | integer, one of 0, 1 |
||
| limit | query | integer | ||
| after | query | string | ||
| where | query | object | where.{column}=value, equality, at most three |
Responses:
| Status | Description |
|---|---|
| 200 | columns, rows, mine (visitors) and opaque next_after cursor; own reads include only the signed-in visitor’s rows |
| 400 | invalid_rows: unknown table, invalid/injected column, order, cursor or scalar value |
| 401 | sign_in_required |
| 403 | forbidden, csrf_required or site_locked |
POST /v1/sites/{sitename}/storage/sqlite/{name}/tables/{table}/rows
Checked against the table’s add value. One JSON object of real column names and scalar values. The server builds INSERT OR ABORT with bound values; visitor-supplied primary keys and rowids are refused. SQLite assigns the IDs and RETURNING supplies the result. Visitor visitor_id, created_at and updated_at are ignored; visitor_id is filled from the site session and created_at with UTC time when the columns exist. Declared SQLite foreign keys must point at a row the visitor may read under the parent table’s read value (for own, a row they added), checked inside the insert transaction. Missing and unreadable parents both return 404 invalid_reference; the insert is rolled back. Optional references must be all NULL; partial composite NULLs are refused. A trigger or foreign-key action that writes another table refuses the insert (403). No visitor SQL accepted. Where add is checked (the comments preset), a caller other than the owner sends the one-time Turnstile token in X-SH-Turnstile (auth.js does it on its own); without one 403 spam_check_required, a failed, reused, expired or mismatched one 403 spam_check_failed, Cloudflare unreachable 403 spam_check_unavailable. The token must have been solved in the frame at GET /v1/turnstile/frame for this host and resource. Such a post is at most SPAM_CHECKED_POST_MAX_BYTES (413 post_too_large) with at most SPAM_CHECKED_POST_MAX_LINKS links (400 too_many_links), and RATE_LIMIT_SPAM_CHECKED_IP and RATE_LIMIT_SPAM_CHECKED_SITE apply (429). The same holds for adding a KV key. The query param for (owner, or the owner signed in on the site’s own address, only; SIM-358, 2026-11-06, review fixes 2026-11-06) adds this row for one other signed-in visitor instead of the caller, by their visitor_id or the email they signed in with, only on a table whose read is own (403 for_requires_own on any other table, checked on the table’s own matrix). It is refused (404 visitor_unknown) if they have never signed in on this site, or (400 visitor_email_ambiguous) if more than one signed-in person currently matches that email; there is no pre-sign-in email binding. The server ignores any visitor_id or written_by the caller sends and sets them itself, visitor_id to that visitor and written_by (added lazily, under the write lock, the first time a table is used this way; 409 written_by_blocked if a trigger or view appears to depend on the table) to owner, otherwise the empty string, which is also written_by’s value whenever for is absent, including a plain owner add. A row added this way does not fire record.added to the owner’s own subscriptions.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| Idempotency-Key | header | string | Opaque key for a create request. For 24 hours, the same authenticated account, POST path and query, key and exact request body replay the original handler status and body, including errors, without executing it again. A different body under that key returns 409 idempotency_key_reused. Concurrent identical requests wait for the first response. Stored only in this server’s memory; a restart clears it. Authentication and rate limiting still apply to replays. A replay does not extend a minted link’s expiry; use a new key to request a new link. Storage row creates support owner credentials and signed-in visitors; anonymous writes ignore this header. Storage replay also checks current resource permissions and is scoped to the resolved site. | |
| sitename | path | string | yes | |
| name | path | string | yes | |
| table | path | string | yes | |
| for | query | string | Owner (or owner-on-site) only. A visitor_id, or the email they signed in with, naming who this row is for. |
Responses:
| Status | Description |
|---|---|
| 200 | changes and last_insert_id |
| 400 | invalid_rows: unknown table, invalid/injected column, order, cursor or scalar value; visitor_email_ambiguous: for’s email currently matches more than one signed-in person |
| 401 | sign_in_required |
| 403 | forbidden, csrf_required, site_locked, or for_requires_own (for on a table whose read is not own) |
| 404 | invalid_reference (parent missing or belongs to another visitor; identical response) or visitor_unknown (for names someone who has never signed in on this site) |
| 409 | row_conflict (generic uniqueness or constraint refusal), written_by_blocked (for’’s lazy column upgrade refused because a trigger or view appears to depend on this table), or written_by_tampered (a trigger changed visitor_id or written_by as a side effect of this for insert; rolled back, SIM-358 round 2) |
| 429 | rate_limited |
| 503 | storage_busy |
| 507 | site_full or sqlite_full: quota precheck or engine allocation failure |
GET /v1/sites/{sitename}/storage/sqlite/{name}/tables/{table}/rows/{id}
Checked against the table’s read value; own reads add visitor_id = caller, so another person’s row is 404 like a missing one. Simple Host only.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| table | path | string | yes | |
| id | path | integer | yes |
Responses:
| Status | Description |
|---|---|
| 200 | {columns,row,mine} |
| 404 | row_not_found |
PATCH /v1/sites/{sitename}/storage/sqlite/{name}/tables/{table}/rows/{id}
Checked against the table’s edit value. A JSON object of real column names and scalar values; id, primary keys, visitor_id, created_at and updated_at are refused (400 server_column). The server builds UPDATE OR ABORT … WHERE rowid = ? [AND visitor_id = caller] and stamps updated_at when the column exists; zero rows changed is 404. Foreign keys are checked as on add. A trigger or foreign-key action that writes another table refuses the edit (403). Visitor writes need the site’s own origin and X-SH-CSRF.
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| table | path | string | yes | |
| id | path | integer | yes |
Responses:
| Status | Description |
|---|---|
| 200 | {changes} |
| 400 | invalid_rows or server_column |
| 401 | sign_in_required |
| 403 | forbidden |
| 404 | row_not_found or invalid_reference |
DELETE /v1/sites/{sitename}/storage/sqlite/{name}/tables/{table}/rows/{id}
Checked against the table’s delete value. DELETE … WHERE rowid = ? [AND visitor_id = caller]; zero rows is 404. A foreign-key action (ON DELETE CASCADE) or trigger that writes another table refuses the delete (403).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| name | path | string | yes | |
| table | path | string | yes | |
| id | path | integer | yes |
Responses:
| Status | Description |
|---|---|
| 200 | deleted: true |
| 403 | forbidden |
| 404 | row_not_found |
GET /v1/sites/{sitename}/storage/usage
Host KV/SQLite share 10000000 bytes per website; files have a separate 10000000-byte allowance. The single-upload default remains 1000000 bytes. Deployed website assets and legacy saved data have separate limits. SQLite main database bytes are counted after checkpoint; transient WAL is excluded. KV key bytes count too. File buckets have at most SITE_STORAGE_FILES_MAX_OBJECTS (default 1000) committed objects, including empty files.
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes |
Responses:
| Status | Description |
|---|---|
| 200 | {used_bytes,limit_bytes,remaining_bytes,files_used_bytes,files_limit_bytes,files_remaining_bytes,breakdown:{kv_bytes,sqlite_bytes,files_bytes}} |
| 403 | forbidden |
GET /v1/sites/{sitename}/storage/visitors
Owner only (key or connector). Turns visitor_id values from this site’s saved data (the visitor_id column of read-own SQLite tables; visitor_id on KV keys and stored files, which only the owner’s reads show) into the email each person signs in with, and, with email instead of (or combined with) id (SIM-358, 2026-11-06), the other direction: the visitor_id they signed in as, for a for request or ResolveOwnerFor directly. Only people who signed in on this site as visitors resolve (recorded by the server when a visitor session starts, never read from the site’s own tables; found false otherwise; no pre-sign-in email binding). 1 to 100 ids and/or emails total, comma-separated or repeated.
Auth: an API key (X-API-Key header, or a REST-audience connector token as Authorization: Bearer).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sitename | path | string | yes | |
| id | query | string | visitor_id values, comma-separated (or repeat id=) | |
| query | string | Email addresses, comma-separated (or repeat email=). Combine with id, or give this alone. |
Responses:
| Status | Description |
|---|---|
| 200 | visitors: [{visitor_id?, email?, found}], one entry per id then per email, in that order |
| 400 | invalid_visitor_id |
| 403 | forbidden (not the owner) |