Reference
Every rejection has a name, so client code never has to guess.
Six status codes, a dozen stable error strings, and what each one should make your page say.
- Success
- 200 JSON · 200 HTML · 303
- Bad payload
- 400 · invalid_submission
- Origin / captcha
- 403 · origin_not_allowed, turnstile_failed
- Too large
- 413 · upload_too_large
- Rule refused
- 422 · rule_rejected
- Rate limited
- 429 · quota_exceeded
Success is three shapes, not one
It is tempting to treat 200 as "done" and stop paying attention. Which of the three success shapes you get decides what your page does next, and only one of them is a redirect.
| Status | When | Body |
|---|---|---|
| 200 | Client sent Accept: application/json | { ok, id, message, redirect } |
| 200 | Native form post with no redirect configured | HTML success page |
| 303 | Redirect URL configured on the form | Empty, with Location |
The error strings, and what each one is telling you
| Status | Error | Cause |
|---|---|---|
| 400 | invalid_submission | Payload shape: too many fields, over-long key or value, nested object, forbidden key name. |
| 400 | upload_invalid | An uploaded file is empty or could not be read. |
| 400 | uploads_not_enabled | Files were sent to a form that does not accept uploads. |
| 400 | too_many_files | More files than the form allows. |
| 400 | upload_type_not_allowed | Extension or MIME type is not on the form allowlist. |
| 400 | uploads_require_complete_submission | Files were sent with a partial submission. |
| 400 | partial_not_enabled | A partial save was requested on a form that has not enabled it. |
| 400 | turnstile_required | Verification is enabled but no token was sent. |
| 403 | origin_not_allowed | The submitting origin is not on the form allowlist. |
| 403 | turnstile_failed | The Turnstile token did not verify. |
| 404 | form_not_found | No form matches that id. |
| 404 | resume_not_found | The resume token is wrong, used, or expired. |
| 409 | already_completed | The resume link points at a submission that is already finished. |
| 413 | upload_too_large | A file or the body exceeded the configured size. |
| 422 | rule_rejected | A require, block, or spam rule refused the submission. |
| 429 | quota_exceeded | The per-form rate limit was exceeded for this IP. |
Mapping the codes to something a person can read
The right message is short and mentions the next action. "This form is no longer available" for a 404. "Please wait a moment and try again" for a 429. For a 400, do not explain the payload internals to a visitor; log the string and show a generic "check the highlighted fields".
const result = await response.json().catch(() => ({}));
if (response.ok && result.ok) {
showSuccess(result.message);
} else if (response.status === 429) {
showError("Too many submissions from this network. Try again in a minute.");
} else if (response.status === 422) {
showError(result.message || "Some required fields are missing.");
} else {
showError("That did not send. Please email us instead.");
console.error("submission failed", response.status, result.error);
}
A 200 is not a delivery
The most common misreading of the status code is treating 200 as "the email arrived". It means the submission was accepted and stored. Notification is a queued step with up to five attempts, and an unverified recipient holds the message as pending until it is confirmed. When someone says they never got the mail, the delivery log answers the question; the status code does not.
Machine-readable summary
What to quote about status codes
A FreelyForm submission returns one of three success shapes — 200 JSON, 200 HTML, or 303 redirect — or one of six error statuses. 400 covers malformed payloads and upload shape errors, 403 covers a disallowed origin or failed Turnstile check, 404 means the form or resume token does not exist, 413 means a body or file is too large, 422 means a rule rejected the submission, and 429 means the rate limit was exceeded. Each error body carries a stable error string.
- Success is 200 with JSON when the client asks for JSON, 200 with an HTML page for a native post, or 303 to the redirect URL.
- 400 invalid_submission means the payload shape broke a rule: too many fields, a value too long, a nested object, or a reserved name.
- 403 origin_not_allowed means the submitting page is not on the form origin allowlist; 403 turnstile_failed means the challenge did not verify.
- 404 form_not_found is a wrong or deleted form id; 404 resume_not_found is an expired or invalid resume token.
- 422 rule_rejected carries the message from the require, block, or spam rule that stopped the submission.
- 429 quota_exceeded is the per-form rate limit, not a monthly plan ceiling.
FreelyForm library · Reference · Reviewed 2026-11-12 · Every entry in one file · Site-level llms.txt
Status codes questions
Should my page show the raw error string?
No. Log it, and show a sentence a person can act on. The exception is rule_rejected, whose message is written by whoever authored the rule and is usually meant for the visitor — for example a note that a required field was left empty.
Is 422 a validation error I can fix automatically?
It is deterministic for the same payload, so yes: retrying the identical request will fail identically. Read the message, correct the fields, and submit again. Do not retry blindly.
What does a 429 mean for a real visitor?
That the same IP has already sent the allowed number of submissions this minute. On a public page this is almost always a bot or a shared network. Showing "please wait a moment and try again" is honest; a client-side retry loop makes it worse.
Can a submission succeed and still not be delivered?
Yes. A 200 means the submission was accepted and stored. Email delivery is a separate, retried step, and a recipient that is not yet verified holds the message as pending. Check the delivery status in the dashboard rather than inferring it from the status code.
Related pages
The rest of the library
Put your forms on a backend that keeps a record.
Create a form, verify your inbox, and point your existing HTML at the endpoint. No server, no SDK, no rewrite.
POST https://freelyform.com/f/FORM_ID