FreelyForm

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.

StatusWhenBody
200Client sent Accept: application/json{ ok, id, message, redirect }
200Native form post with no redirect configuredHTML success page
303Redirect URL configured on the formEmpty, with Location

The error strings, and what each one is telling you

StatusErrorCause
400invalid_submissionPayload shape: too many fields, over-long key or value, nested object, forbidden key name.
400upload_invalidAn uploaded file is empty or could not be read.
400uploads_not_enabledFiles were sent to a form that does not accept uploads.
400too_many_filesMore files than the form allows.
400upload_type_not_allowedExtension or MIME type is not on the form allowlist.
400uploads_require_complete_submissionFiles were sent with a partial submission.
400partial_not_enabledA partial save was requested on a form that has not enabled it.
400turnstile_requiredVerification is enabled but no token was sent.
403origin_not_allowedThe submitting origin is not on the form allowlist.
403turnstile_failedThe Turnstile token did not verify.
404form_not_foundNo form matches that id.
404resume_not_foundThe resume token is wrong, used, or expired.
409already_completedThe resume link points at a submission that is already finished.
413upload_too_largeA file or the body exceeded the configured size.
422rule_rejectedA require, block, or spam rule refused the submission.
429quota_exceededThe 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".

Handling the outcome
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.

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