Skip to content

API overview

The backend exposes two cleanly separated HTTP surfaces:

  • Public runtime API — /public/v1/**. What the embed calls: fetch the live questionnaire, create a response session, save answers, upload files, complete, geocode. Designed to face the internet; no authentication — the unguessable responseId is the only credential a session holds.
  • Management API — /api/v1/**. What the editor calls: questionnaire CRUD, drafts, publishing, versions, responses, CSV export, file downloads. No authentication — you must network-protect it. See the security posture.

The endpoint reference pages are generated from the backend’s OpenAPI document (download the raw spec); a running backend also serves it live at /v3/api-docs with an interactive UI at /swagger-ui.html. The pages here add the semantics the spec doesn’t carry: the response lifecycle and answer JSON shapes.

Every application-level error returns one JSON shape:

{
"status": 409,
"message": "a completed response already exists for this reference",
"errors": [],
"code": "ALREADY_SUBMITTED"
}
  • message — human-readable summary.
  • errors — detail list for validation failures (always present, often empty). Validation is aggregated: one bad request returns all its problems at once.
  • code — machine-readable, present only where a client needs to branch. Exactly one exists today: ALREADY_SUBMITTED (409) for the one-per-reference submission policy.

Two operational exceptions to the shape: rate-limited requests return 429 {"status":429,"message":"too many requests","errors":[]} from the filter itself, and a storage-backend outage during a file operation surfaces as a generic 500.

Per-IP token buckets; the management API is never rate-limited.

Bucket Applies to Default
General all mutating /public/v1 requests except uploads burst 20, refill 10/s
Uploads POST …/files burst 30, ≈1 per 20 s (≈30 / 10 min); 429s carry Retry-After
Geocode GET /public/v1/geocode burst 10, 2/s
Ref-status GET …/ref-status burst 10, 1/s

Non-mutating public reads (the live definition, session rehydration) are not rate-limited. See Environment variables for tuning and the reverse-proxy caveat about client IPs behind proxies.

  • Ids: questionnaires have an internal id (used in management paths) and a publicId (q_ + 10 characters, used in public paths and embeds). Responses use responseId (r_ + 32 hex characters). Files use fileId (f_ + 32 hex characters). Storage keys are never exposed.
  • Timestamps are ISO-8601 UTC strings.
  • Management CORS is allow-all — the assumption, again, is that the network layer guards it.
  • OPTIONS preflights to /public/** always succeed; real enforcement happens on the actual request (allowed-origin mismatches → 403 on mutations).