One artifact model, honest by construction.
Every document on stub.cafe is an artifact: an immutable id, a kind, a version, a source class, an authority status and a content hash. The API, when it ships, exposes exactly that and nothing it cannot back.
The artifact record
This is the shape the browser builders write today into stub_artifacts_v1, and the shape the hosted service will store. The fields that matter most are the ones that refuse to lie.
source_class:user_suppliedtoday;employer_attestedandissuer_integratedonly with the proof behind them.authority_status:draftorgenerated.issuer_verifiedneeds a signed issuer response.transferability:not_applicablefor everything except tickets, which areunknownuntil an issuer sayspermitted_by_issuer.financial_status:unpaidorpayment_self_reportedtoday;payment_recordedonly from a signed processor event.content_digest: SHA-256 of the canonical JSON of the inputs, recomputed on every version.
artifact:
id: "inv_7q4m2xh9pl"
tenant_id: "local-browser"
kind: "invoice" # event_ticket | keepsake | pay_statement
# | remittance_voucher | check_fill | invoice | receipt
version: 2
source_class: "user_supplied"
authority_status: "generated"
transferability: "not_applicable"
financial_status: "payment_self_reported"
content_digest: "sha256:3f0a91c2d4e5…"
history:
- version: 1
digest: "sha256:91c2d4e5b6a7…"
status: "generated"
created_at: "2026-10-27T13:04:11Z"
What exists today
One endpoint. Everything else runs in the browser. Anything not listed here does not exist.
| Method and path | Contract |
|---|---|
GET /api/intake.php | Liveness and the list of accepted actions. |
POST /api/intake.php | JSON {action, email, …fields, idempotency_key} with action one of pilot, business, paystubs, exchange, report, contact. Returns 200 {ok, receipt_id, action, message}; 422 on validation, 429 with Retry-After over 30 per hour per connection, 405 otherwise. A repeated Idempotency-Key (8 to 128 chars, 7 days) returns the first receipt with duplicate: true and stores nothing new. Every response is private, no-store. |
/blogs/api/… | The shared blog engine's read API, identical on every cafe site. |
Planned v1 API
Versioned under /v1, standardized machine error codes with safe user messages, idempotency keys on every write, request-size limits, schema validation and audit logs. Public credentials are never downloadable by id alone. Ships with the business pilot, to pilot businesses first.
Every "authorized" reference below is a placeholder for a real signed relationship, not a claim that one exists.
POST /v1/auth/sessions
GET /v1/me
POST /v1/orgs
POST /v1/orgs/{id}/verify
GET /v1/templates?kind=ticket
POST /v1/artifacts/drafts
PATCH /v1/artifacts/{id}/draft
POST /v1/artifacts/{id}/preview
POST /v1/artifacts/{id}/finalize
GET /v1/artifacts/{id}
POST /v1/artifacts/{id}/download-session
POST /v1/events
POST /v1/events/{id}/tickets/issue
POST /v1/tickets/{id}/check-in
POST /v1/invoices
POST /v1/orders
POST /v1/webhooks/payments/{provider}
Ticket tokens, as built today
A ticket token is <10-char id>.<16 hex>: the ticket id in a scan-safe alphabet (no 0, O, 1 or I) and the first 64 bits of an HMAC-SHA-256 over ticket_id|event_id with a browser-local secret. The QR payload is STUB1:<token>. The validator recomputes the signature, then checks the ticket's live status: a correct signature on a revoked ticket is still rejected. The hosted pilot moves the secret server-side; the format stays.
The QR encoder is our own implementation of ISO/IEC 18004 (byte mode, level M, versions 1 to 10), verified module-for-module against a reference library for payloads across every version. No third-party code runs on the site.