Skip to content

Data API ​

A data-plane REST API for reaching queues (pull, like SQS/Pub-Sub) and databases (synchronous SQL, like BigQuery) from outside a worker — a backend in another language, a CI job, a script or an analytics tool. It lives on the platform host (hosts.platform) under /v1/data/.

Resources are never created, deleted or configured here — only used. Create and configure queues and databases in the console or the management API.

Authentication ​

Every call needs an API token:

Authorization: Bearer tot_<id>_<secret>
  • Session cookies are refused (401), which keeps the API CSRF-free, and CORS is off — tokens are server-side credentials and must not be put in browser code.
  • A token is scoped to one organisation and optionally narrowed to projects and a maximum role; its effective role is token scope ∩ the creator's current membership, re-checked on every request.
  • A token whose creator left the organisation returns 404 (it no longer reaches the org); a revoked or expired token, or a session cookie, returns 401.

Create a token in the console (Tokens page) with at least the developer role on the project.

Paths ​

/v1/data/{org}/{project}/{env}/queues/{slug}/{messages|receive|ack|retry|extend}
/v1/data/{org}/{project}/{env}/queues/{slug}                       (GET: describe)
/v1/data/{org}/{project}/{env}/databases/{slug}/query
/v1/data/{org}/{project}/{env}/databases/{slug}/query/{pageToken}  (GET: next page)
/v1/data/{org}/{project}/{env}/databases/{slug}/tables
/v1/data/{org}/{project}/{env}/databases/{slug}/tables/{name}
/v1/data/{org}/{project}/{env}/databases/{slug}/tables/{name}/rows (POST: insert)

{org}, {project}, {env} and {slug} are the names you already use. Anything outside the token's scope is reported as not found, indistinguishable from a name that does not exist.

Roles ​

OperationMinimum role
Queue: send, receive, ack, retry, extenddeveloper
Queue: describe (depth, failed count)viewer
Database: read-only query, list/describe tablesviewer
Database: write query (DML), insert rowsdeveloper
Database: DDLadmin or owner

A project-scoped token is capped at developer, so it can never run DDL.

Queues ​

Method & pathBody → response
POST …/queues/{slug}/messages{messages: [{body, contentType?, delaySeconds?, attributes?, dedupId?}]} (1–100) → {ids: [...]} (201)
POST …/queues/{slug}/receive{max?: 1–100 (10), visibilitySeconds?: 1–43200 (30), waitSeconds?: 0–20 (0)} → {messages: [{id, receipt, body, contentType, attributes, attempts, enqueuedAt}]}
POST …/queues/{slug}/ack{receipts: [...]} → {acked, expired: [...]}
POST …/queues/{slug}/retry{receipts: [...], delaySeconds?} → {retried, expired: [...]}
POST …/queues/{slug}/extend{receipts: [...], visibilitySeconds} → {extended, expired: [...]}
GET …/queues/{slug}→ {slug, depth, inFlight, failedDepth, oldestAgeMs, consumer: "app" | "pull"}
  • Pull only. A queue with a consumer app refuses receive with 409 queue has an app consumer. Clear the consumer (console "Pull over the API") to pull it.
  • At-least-once with leases: a received message is invisible until its receipt is acked, retried, or the visibility timeout ends, after which it is redelivered with attempts + 1. Past maxRetries it moves to the queue's failed area.
  • Receipts are the lease tokens. A receipt that is stale, malformed, from another queue, or already acked is returned in expired — never an error for the whole batch, so ack/retry/extend are idempotent.
  • Long poll: waitSeconds > 0 holds the request until a message is visible or the wait ends. At most 50 open long polls per organisation (and 1,000 host-wide); over either is 429.
  • Body encoding: JSON value for json, string for text, base64 for bytes.
bash
curl -s -XPOST https://console.example.com/v1/data/acme/shop/production/queues/orders/messages \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"messages":[{"body":{"id":1}}]}'

curl -s -XPOST https://console.example.com/v1/data/acme/shop/production/queues/orders/receive \
  -H "Authorization: Bearer $TOKEN" -d '{"max":10,"waitSeconds":20}'
# then ack the receipts you processed:
curl -s -XPOST https://console.example.com/v1/data/acme/shop/production/queues/orders/ack \
  -H "Authorization: Bearer $TOKEN" -d '{"receipts":["..."]}'

Databases ​

Method & pathBody → response
POST …/databases/{slug}/query{sql, params?: [...], maxRows?: 1–10000 (1000), timeoutSeconds?: 1–30 (30)} → {columns: [{name, type}], rows: [[...]], rowCount, nextPageToken?, affectedRows?}
GET …/databases/{slug}/query/{pageToken}→ next page, same shape
GET …/databases/{slug}/tables→ tables and views (same shape as the console database viewer)
GET …/databases/{slug}/tables/{name}→ columns, keys, DDL
POST …/databases/{slug}/tables/{name}/rows{rows: [{col: value, ...}]} (1–1000 rows, ≤ 4 MiB) → {inserted}
  • One statement per call, classified by its first keyword: select/with are reads (viewer), insert/update/delete/merge/upsert/replace are DML (developer), create/drop/alter/truncate/rename are DDL (admin). Anything else (BEGIN, COMMIT, …) is 400. A with statement that contains a write keyword (insert, update, delete, merge, create, drop, …) outside string literals, quoted identifiers and comments counts as DML. A read is always wrapped as SELECT * FROM (…) LIMIT … OFFSET …, so a statement that only looks like a read cannot write.
  • params are bound by ? placeholders, never concatenated.
  • Paging: a result larger than maxRows returns a nextPageToken (opaque, bound to the token and database, valid 10 minutes, held in host memory and lost on restart). There is no snapshot — a later page may reflect writes made in between. A page is also cut at 8 MiB (UTF-8) and continues with a token. Column type is inferred from the page's values (BIGINT, DOUBLE, DECIMAL, BOOLEAN, VARCHAR, BYTES, or NULL when all null); integers past 2^53 and decimals come back as strings.
  • Insert rows is one transaction: all rows or none. Values are checked against the column's catalog type first; a bad row returns 400 with its row index. Inserts are not cut short by a timeout; they wait at most 30 s for the write lock, otherwise 503 busy (nothing written). Insert rows is a console-style data write, so the instance's consoleDataWrites setting (instance admin) can turn it off — then it returns 403 forbidden. Write query statements are not affected by that setting.
  • Timeouts: past timeoutSeconds a query returns 504. The engine has no statement cancellation, so a timed-out write may still commit — the response says 504 but the statement may finish. Reads are safe (wrapped and read-only).
  • Concurrency: at most 4 running statements per database; over that is 429.
  • Analytics. The slug analytics is the org's Workers Analytics Engine database. It is read-only for every role. It is per org, so a project-scoped token reads the whole org's analytics, not only its project's apps. Image resizing has no WebP or AVIF encoder: those output formats fall back to JPEG (JPEG sources) or PNG (everything else).
bash
curl -s -XPOST https://console.example.com/v1/data/acme/shop/production/databases/shop/query \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"sql":"SELECT id, name FROM users WHERE active = ?","params":[1],"maxRows":500}'

curl -s -XPOST https://console.example.com/v1/data/acme/shop/production/databases/shop/tables/events/rows \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"rows":[{"id":1,"kind":"click"},{"id":2,"kind":"view"}]}'

Errors ​

Errors are {error, code} with these statuses (a 429 carries Retry-After: 1):

StatuscodeMeaning
400bad_requestmalformed body, bad statement, bad insert row ({row})
401unauthenticatedmissing, invalid, expired or revoked token; a session cookie
403forbiddenrole too low
404not_foundunknown resource, or outside the token's scope; also unknown or expired page token, table not found
409conflictreceive on a queue with an app consumer
413too_largebody over the size limit
429rate_limitedthe token's budget or a long-poll/concurrency cap
503suspendedthe organisation is suspended
503busyan insert waited 30 s for the database write lock; nothing was written, retry
504timeouta query ran past timeoutSeconds
507queue_fullthe queue is at maxDepth/maxBytes

Limits and auditing ​

  • Rate limit: every /v1/data/* call (every receive included) counts against one budget per API token — 36,000 requests per 15-minute window (~40 req/s), shared across queues and databases.
  • Reserved headers: inbound x-totis-* request headers are stripped by the host.
  • Audited in the org's audit log: DDL, DML and insert-rows (statement kind, table, row count — never SQL text or values). Plain reads, sends and receives are counted per token (shown as "last used"), not audited individually.
  • A suspended organisation gets 503 on every call.

Not in the MVP ​

Push subscriptions/webhooks, topics/fan-out, async query jobs, exports to object storage, BigQuery or Pub/Sub wire compatibility, client SDKs, per-resource keys, and browser/CORS access.

TinyS3 is AGPLv3 licensed. Cloudflare and Cloudflare Workers are trademarks of Cloudflare, Inc. Amazon S3 and AWS are trademarks of Amazon.com, Inc. or its affiliates. MinIO is a trademark of MinIO, Inc. They are named only to describe compatibility; totis.ai is not affiliated with, endorsed by, or sponsored by any of them.