Public API

Base URL: https://api.forms.zone. No authentication — these are the endpoints your site calls directly (the SDK wraps all of them).

Every response is JSON. Errors look like { "error": { "code": "...", "message": "..." } }. Rate limits: 30 requests/60s per visitor IP on submit and upload-authorize, 120 requests/60s per form on submit.

GET /v1/forms/:slug

Fetches a published form's schema.

200 { slug, name, version, schema: { fields }, maxUploadSizeBytes }
404 not_found · 403 origin_not_allowed

POST /v1/forms/:slug/submit

Submits the form. turnstileToken is required on every call. Set draft: true to save progress without finalizing — required fields aren't enforced on a draft, but types/formats still are.

body { data: {...}, draft?: boolean, turnstileToken: string }
201 { success, submission_id, edit_url, edit_expires_at }
429 rate_limited · 400 turnstile_failed · 400 validation_failed · 403 origin_not_allowed

edit_url contains a one-time token in plaintext — that's the respondent's only copy of it, so store/send it then, not later.

POST /v1/uploads/authorize

Step one of an image upload. Capped at 1 MiB or the form's configured limit, whichever is smaller. Accepts image/jpeg, image/png, image/webp.

body { slug, contentType, sizeBytes }
200 { uploadUrl, r2Key, contentType, expiresAt }
400 file_too_large · 429 rate_limited

PUT the raw file bytes to uploadUrl (valid for 10 minutes, no extra auth needed), then pass the returned r2Key as that field's value when you submit. The file is checked against its real bytes — not the content-type you sent — before the submission is accepted.

POST /v1/submissions/access

Exchanges an edit token (from an edit_url) for a submission id.

body { token }
200 { submission_id } · 401 invalid_token

GET /v1/submissions/:id/edit

Header: Authorization: Bearer <token>.

200 { id, formId, formVersion, data, status, revision }
401 unauthorized · 404 not_found

PATCH /v1/submissions/:id

Updates a submission as its respondent. Validated against the form version it was originally submitted against. A draft can finalize here (draft: false or omitted); a submitted response can never go back to draft.

header Authorization: Bearer <token>
body { data, draft? }
200 { id, data, status, revision }
401 unauthorized · 400 validation_failed · 400 invalid_transition