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 unguessableresponseIdis 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.
Error format
Section titled “Error format”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.
Rate limits (public API only)
Section titled “Rate limits (public API only)”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.
Conventions
Section titled “Conventions”- Ids: questionnaires have an internal id (used in management paths) and a
publicId(q_+ 10 characters, used in public paths and embeds). Responses useresponseId(r_+ 32 hex characters). Files usefileId(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.
OPTIONSpreflights to/public/**always succeed; real enforcement happens on the actual request (allowed-origin mismatches → 403 on mutations).