Skip to content

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.

GET /recon.config.json

Returns 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.

PUT /v1/apps/{id}/kv/{key}
Authorization: Bearer <token>
Content-Type: application/json
X-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.

POST /v1/apps/{id}/counter/{key}
Authorization: Bearer <token>
Content-Type: application/json
X-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,c
Authorization: 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.

POST /v1/apps/{id}/collect/{bucket}
Authorization: Bearer <token>
Content-Type: application/json
X-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=0
Authorization: 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=0
Authorization: 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.

POST /v1/apps/{id}/analytics
Authorization: Bearer <token>
Content-Type: application/json

The 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).

OperationAuthorization: BearerX-Recon-NonceOrigin 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.

LimitValue
Request body1 MB per write
KV keys per app1,000
KV value size64 KB
Counters per app1,000
Collection records per bucket100,000
Collection record size64 KB
Writes per app + IPrate-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.

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.

Hosted on Bailey