Skip to content

HTTP API

A deployment describes itself. The API lives at https://api.<zone>, versioned under /v1, and three routes need no token:

  • /v1/health — {"ok":true,"mode":"secret","zone":"<zone>","version":"0.4.0-dev","build":"10ebe34"}; what handbill doctor probes. version is the deployed Worker’s package version and build its short commit; they arrive as deploy-time vars, so a run started without them — wrangler dev, a self-hosted deploy — omits both rather than sending an empty string.
  • /v1/openapi.json — the OpenAPI 3.1 document, generated from the contract the Worker implements and the CLI is built from, so it cannot describe an API the deployment does not serve. Point a client generator at it, or read it and write the calls by hand. On the maintainer’s deployment: api.handbill.dev/v1/openapi.json.
  • /docs — at the host root, not under /v1: the same document rendered as a browsable reference, api.handbill.dev/docs.

Everything else takes Authorization: Bearer <token>:

CallWhat it does
PUT /v1/pages/<hash>Publish. The body is the HTML; the server recomputes the hash and rejects a mismatch.
GET /v1/pagesEverything published, newest first.
DELETE /v1/pages/<hash>Unpublish. Idempotent.
GET /v1/accountOwner, tier, and what that key has spent against the tier’s limits. limits is null where nothing is counted.
POST /v1/account/checkoutA checkout URL for the paid tier, the session created by the Worker with the owner taken from the key. 404 where the deployment sells nothing, 409 where the account already pays.
PUT /v1/aliases/<name>Point a name at a page (body {"hash":"…"}). Only with the ALIASES binding.
GET /v1/aliasesEvery alias and the hash it points at. A key listing, so a fresh name can lag by a minute.
GET /v1/aliases/<name>What one name points at, read by key rather than out of that listing. 404 when unset.
DELETE /v1/aliases/<name>The name stops answering; the page stays.
DELETE /v1/keys/currentRevoke the key in the request’s own header. Idempotent. Only with the ACCOUNTS binding.

Four routes are neither public nor bearer-authed, because for each the credential is something other than a key the middleware could resolve. DELETE /v1/admin/pages/<hash> takes a page down and PUT /v1/admin/tier/<owner> (body {"tier":"free"|"paid"}) sets what an account may spend; both check the deployment’s own ADMIN_TOKEN, which is never a user key, and both answer 404 where no such secret is set. POST /v1/billing/webhook is called by the payment provider rather than by anyone holding a handbill credential: its credential is a Standard Webhooks signature over the exact body, everything that verifies is 202, and without a webhook secret the route is 404. And POST /v1/keys takes {"githubToken":"…"}, verifies it with GitHub, and returns a key to use as the bearer token from then on — shown once, stored only as its SHA-256. It exists only on a deployment with the ACCOUNTS binding, which is also what makes /v1/health report "mode":"accounts"; without it the key routes answer 404 and one shared PUBLISH_TOKEN is the token.

Two things the ACCOUNTS binding changes are worth stating plainly. Revocation is idempotent but not instant: DELETE /v1/keys/current returns 204 whether the key was live, already revoked, or never existed, and it revokes only the key in that request’s own header — but the key keeps working for up to a minute afterwards, because key records are read through KV’s edge cache (the same ~60s window the alias listing has). Aliases are operator-only in accounts mode: with keys in play, every /v1/aliases route except the owner-filtered listing answers a normal key with 404, so hosted users get hash URLs and only the operator’s own token manages names.

Errors are JSON with a _tag: HashMismatch (400), Unauthorized (401), NotFound (404), AlreadyPaid (409), TooLarge (413), QuotaExceeded (429). The last one names which limit tripped, what it allows and — for the daily page count — when it resets; a deployment that counts nothing never raises it. The contract is versioned under /v1 and does not break inside a major version; published links never change.