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, returns401.
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
| Operation | Minimum role |
|---|---|
| Queue: send, receive, ack, retry, extend | developer |
| Queue: describe (depth, failed count) | viewer |
| Database: read-only query, list/describe tables | viewer |
| Database: write query (DML), insert rows | developer |
| Database: DDL | admin or owner |
A project-scoped token is capped at developer, so it can never run DDL.
Queues
| Method & path | Body → 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
receivewith409 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. PastmaxRetriesit 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 > 0holds 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 is429. - Body encoding: JSON value for
json, string fortext, base64 forbytes.
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 & path | Body → 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/withare reads (viewer),insert/update/delete/merge/upsert/replaceare DML (developer),create/drop/alter/truncate/renameare DDL (admin). Anything else (BEGIN,COMMIT, …) is400. Awithstatement 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 asSELECT * FROM (…) LIMIT … OFFSET …, so a statement that only looks like a read cannot write. paramsare bound by?placeholders, never concatenated.- Paging: a result larger than
maxRowsreturns anextPageToken(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. Columntypeis inferred from the page's values (BIGINT,DOUBLE,DECIMAL,BOOLEAN,VARCHAR,BYTES, orNULLwhen 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
400with itsrowindex. Inserts are not cut short by a timeout; they wait at most 30 s for the write lock, otherwise503 busy(nothing written). Insert rows is a console-style data write, so the instance'sconsoleDataWritessetting (instance admin) can turn it off — then it returns403 forbidden. Writequerystatements are not affected by that setting. - Timeouts: past
timeoutSecondsa query returns504. 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
analyticsis 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):
| Status | code | Meaning |
|---|---|---|
| 400 | bad_request | malformed body, bad statement, bad insert row ({row}) |
| 401 | unauthenticated | missing, invalid, expired or revoked token; a session cookie |
| 403 | forbidden | role too low |
| 404 | not_found | unknown resource, or outside the token's scope; also unknown or expired page token, table not found |
| 409 | conflict | receive on a queue with an app consumer |
| 413 | too_large | body over the size limit |
| 429 | rate_limited | the token's budget or a long-poll/concurrency cap |
| 503 | suspended | the organisation is suspended |
| 503 | busy | an insert waited 30 s for the database write lock; nothing was written, retry |
| 504 | timeout | a query ran past timeoutSeconds |
| 507 | queue_full | the 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
503on 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.