Data API reference
The JavaScript SDK wraps everything here. Use this page if you’re calling the store from raw fetch, another language, or just want to know exactly what goes over the wire.
All store endpoints live under the same origin as your site: the service serves both your pages and the API.
Token issuance
Section titled “Token issuance”GET /recon.config.jsonReturns a fresh serve-time token for the calling page. Sent with Cache-Control: no-store; call it per page load, don’t cache it.
{ "app_id": "a1b2c3…", "api_base": "", "token": "<serve-time token>"}The token is short-lived (~10 min), bound to the page’s origin, and carries the site’s allowed scopes. Use it as Authorization: Bearer <token> on the calls below. Returns 404 if the host isn’t a known/enabled site.
Key-value
Section titled “Key-value”PUT /v1/apps/{id}/kv/{key}Authorization: Bearer <token>Content-Type: application/jsonX-Recon-Nonce: <single-use uuid>
{ "value": <any JSON> }GET /v1/apps/{id}/kv/{key}Authorization: Bearer <token>GET returns { "value": <json> }, or 404 if the key is absent. KV is readable by anyone (it’s public by visibility); don’t store secrets in it.
Counter (atomic)
Section titled “Counter (atomic)”POST /v1/apps/{id}/counter/{key}Authorization: Bearer <token>Content-Type: application/jsonX-Recon-Nonce: <single-use uuid>
{ "by": 1 }Atomically adds by to the counter and returns { "value": <new total> } (200). by is an integer ≥ 1 and defaults to 1 (an empty body means +1); 0, negatives, and non-integers are rejected with 400. The read-modify-write happens under a per-app lock server-side, so concurrent increments never lose an update, unlike a client-side GET+PUT on KV. There is no decrement and no “set” operation exposed to the page.
GET /v1/apps/{id}/counter/{key}Authorization: Bearer <token>Returns { "value": <n> } (0 if the counter was never incremented). Readable with the page token (no owner key); a counter is public by visibility.
GET /v1/apps/{id}/counters?keys=a,b,cAuthorization: Bearer <token>Batch read: returns { "values": { "a": 3, "b": 0, … } } in one call (max 200
keys, 400 beyond). This is what store.counters(ids) uses to order a public
board by votes without an N+1.
Collection (forms)
Section titled “Collection (forms)”POST /v1/apps/{id}/collect/{bucket}Authorization: Bearer <token>Content-Type: application/jsonX-Recon-Nonce: <single-use uuid>
{ "email": "…", "message": "…" }Appends the JSON body to the bucket. Returns 201. There is no public GET for collection buckets; reads are owner-only (see below).
GET /v1/apps/{id}/collect/{bucket}X-Owner-Key: <owner key>Owner-only read of submissions, authenticated with an HMAC-sealed owner key (not the page token). The CLI’s bailey data uses this path.
GET /v1/apps/{id}/collect/{bucket}?view=public&limit=50&offset=0Authorization: Bearer <token>Public projected read: only for a bucket declared visibility: "public"
(the token then carries a read scope for it; an insert_only bucket returns
403 before any store access). Returns { "records": [...], "total": n },
newest first, paginated (limit default 50, max 200). Each record contains
only the fields declared public, and a strong-PII backstop redacts
identifying values that slipped into a public field. This is what store.list
calls.
GET /v1/apps/{id}/collect/{bucket}?view=self&limit=50&offset=0Authorization: Bearer <token>Private per-member read: only for a bucket declared visibility: "owned"
(a private journal). Returns { "records": [...], "total": n } containing
only the calling member’s own records (newest first, paginated). The member
is identified from the token’s viewer identity (sub), which exists only on
org-access pages — without it the endpoint returns 403, never a fallback to
all rows. The response never includes the member’s identifier. Writing to an
owned bucket likewise requires that identity (403 otherwise). Not even the
site owner can read an owned bucket’s content. This is what store.mine calls.
DELETE /v1/apps/{id}/collect/{bucket}Authorization: Bearer <token>Self-erase (GDPR Art. 17), for an owned bucket only: deletes only the
calling member’s own records and returns { "removed": n }. Requires the same
viewer identity (sub, org pages only) plus origin binding; a member can never
erase another member’s rows, and there is no owner path here. This is what
store.forget calls.
Analytics ingestion
Section titled “Analytics ingestion”POST /v1/apps/{id}/analyticsAuthorization: Bearer <token>Content-Type: application/jsonThe SDK’s auto-instrumentation posts pageviews and named events here
(plan-gated: the serve-time token only carries the analytics:insert scope if
the site’s plan includes analytics). Hits are aggregated at write time into
time-series tallies; no cookie, IP or session is stored. You normally never
call this by hand; reading the report is owner-only (dashboard Analytics
tab, read_analytics MCP tool).
Required headers, by operation
Section titled “Required headers, by operation”| Operation | Authorization: Bearer | X-Recon-Nonce | Origin must match |
|---|---|---|---|
GET /recon.config.json | – | – | (issues for the request’s host) |
PUT kv / POST collect / POST counter (writes) | ✓ | ✓ | ✓ |
GET kv / GET counter / GET counters (read) | ✓ | – | – |
GET collect ?view=public (public read) | ✓ | – | – |
GET collect ?view=self (private per-member read, org pages) | ✓ | – | – |
DELETE collect (member self-erase, org pages) | ✓ | – | ✓ |
POST analytics (SDK auto) | ✓ | – | ✓ |
GET collect (owner read) | use X-Owner-Key instead | – | – |
The X-Recon-Nonce is a fresh value per write (a UUID), single-use; it’s replay protection, not the token’s own nonce.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Request body | 1 MB per write |
| KV keys per app | 1,000 |
| KV value size | 64 KB |
| Counters per app | 1,000 |
| Collection records per bucket | 100,000 |
| Collection record size | 64 KB |
| Writes per app + IP | rate-limited (sliding window) |
Over-limit responses: 413 (too large), 429 (rate limited), 507 (quota exceeded), 404 (unknown/undeclared bucket).
Same-origin calls (the normal case: your page calling its own site) need nothing special. For cross-origin requests, the service reflects Access-Control-Allow-Origin only for origins it recognises as a known site (a …trybailey.app subdomain or a verified custom domain). Allowed methods: GET, POST, PUT, OPTIONS; allowed headers: Authorization, Content-Type, X-Recon-Nonce. Preflight (OPTIONS /v1/…) is handled.
Anti-bot (optional)
Section titled “Anti-bot (optional)”When enabled for a site, writes also require a Cloudflare Turnstile proof via X-Recon-Turnstile. It’s off by default in v0.
The full rationale for tokens, origin binding, nonces and owner keys is in the Security model.