API reference

preview

Every quickform-server endpoint — the two public ones your form page uses, and the admin surface the PWA, the portal and your agents use.

Author: Poul Kjeldager
Platform: linuxmacoswindowsdocker
Category: agents

API reference

Base path is /api. All bodies are JSON. Admin endpoints take Authorization: Bearer <token>; with no QUICKFORM_ADMIN_TOKEN configured the server runs open and the header is ignored.

Two token scopes exist:

  • the admin token (QUICKFORM_ADMIN_TOKEN) — every org; this is a service credential for a portal or an operator, never something a browser holds;
  • an org token, minted per org, which grants exactly that org and nothing else. An org token cannot create a sibling org.

Public — the customer's page

Both are unauthenticated by design and gated by the form's allowedOrigins. Both answer OPTIONS preflight. The CORS response echoes the caller's exact Origin and sets Access-Control-Allow-Credentials: true (never *), because the QuickForm runtime submits with credentialed fetch.

GET /api/orgs/{org}/forms/{form}/definition

Returns the stored definition with a submit block injected:

{
  "intro": { "…": "…" },
  "questions": { "…": "…" },
  "submit": {
    "submitUrl": "https://forms.example.com/api/orgs/acme/forms/contact/submissions",
    "submitMethod": "POST"
  }
}

submitUrl is built from PUBLIC_BASE_URL when set, and from the request host otherwise. Set it whenever anything reaches the server through a proxy or from another container — otherwise a container-to-container call produces an internal host that a browser cannot reach.

POST /api/orgs/{org}/forms/{form}/submissions

Ingest. The body is whatever the QuickForm runtime posts — a flat logicalName → value map, plus a submitFields block when the definition has one. The body is capped at 256 KiB; the raw bytes are stored untouched alongside the parsed answers.

Returns 200 with {"status":"received","id":"…"}. Webhooks and Web Push fan out after the answer is durable, so a dead webhook never costs a submitter their answer.

Admin — orgs

MethodPathNotes
GET/api/orgsList orgs. Token hashes are stripped
POST/api/orgs{slug, name}. Admin token only
GET/api/orgs/{org}One org
PUT/api/orgs/{org}Update name
POST/api/orgs/{org}/tokensMint an org token: {label}

Minting returns the plaintext exactly once. Only hex(sha256(secret)) is persisted, so the server cannot show you the key again — that is the point of storing it that way.

Admin — forms

MethodPathNotes
GET/api/orgs/{org}/formsList forms with counts and timestamps
GET/api/orgs/{org}/forms/{form}{form, definition?, embedUrl}
PUT/api/orgs/{org}/forms/{form}Create or replace metadata
DELETE/api/orgs/{org}/forms/{form}Delete the form and its answers
PUT/api/orgs/{org}/forms/{form}/definitionStore a definition (2 MiB cap)

PUT …/forms/{form} takes:

{
  "name": "Contact",
  "description": "",
  "allowedOrigins": ["https://acme.example"],
  "webhooks": [{ "url": "https://…", "secret": "whsec_…" }],
  "definition": { "…": "…" }
}

Two behaviours worth knowing, because they differ:

  • name, description, allowedOrigins and webhooks are replaced unconditionally — a PUT that omits webhooks clears them.
  • definition is applied only when present and non-empty. Omit it and the stored definition (and its version) is left alone. Sending an unchanged definition would otherwise mint a byte-identical new version.

The embedUrl on GET …/forms/{form} is the definition URL — the single thing a customer's developer needs.

Admin — submissions

MethodPathNotes
GET/api/orgs/{org}/submissions?limit=NCross-form feed of summaries
GET/api/orgs/{org}/forms/{form}/submissions?limit=NOne form's summaries
GET/api/orgs/{org}/forms/{form}/submissions/{id}The full answer set
POST/api/orgs/{org}/forms/{form}/submissions/{id}/readMark as read

A summary carries {id, org, form, receivedAt, read, preview} — enough for a feed row without shipping every answer of every submission. The full record adds answers, submitFields, origin, referer, userAgent, ip and the raw body.

read is a human-facing flag set by the PWA or the portal. Agents ignore it; they are driven by the webhook, not by a queue they have to drain.

Push

MethodPathNotes
GET/api/push/vapidThe public VAPID key for the PWA
POST/api/orgs/{org}/push/subscribeStore a subscription
POST/api/orgs/{org}/push/unsubscribeDrop one

Keys are generated into USER_DATA_DIR on first boot. The notification payload names the org and the form and nothing else — the answers are fetched when a person opens the app, never carried through a push service.

Service

MethodPathNotes
GET/api/healthLiveness
GET/api/versionBuild version and uptime

Everything not under /api is served from WEB_DIR — the PWA.