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