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_supplied today; employer_attested and issuer_integrated only with the proof behind them.
  • authority_status: draft or generated. issuer_verified needs a signed issuer response.
  • transferability: not_applicable for everything except tickets, which are unknown until an issuer says permitted_by_issuer.
  • financial_status: unpaid or payment_self_reported today; payment_recorded only 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 pathContract
GET /api/intake.phpLiveness and the list of accepted actions.
POST /api/intake.phpJSON {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.