Catch a bad Submission before Send with Preflight
Ask the Form's own judgment while the person is still on the page. Preflight checks request shape, Origin and Strict schema, stores no row, and never promises the real Submission will be accepted.

A refused Submission costs the one thing this product cannot recover: a person who was on the page and then was not. FormFlow keeps every refusal as a record you can act on, and a Form's health signals count them. Preflight is the only part of that judgment that runs before Send. It arrived with v1.0.0.
What a Preflight answers
Send a POST to /api/v1/f/{token}/preflight with the answers your form would have posted, as JSON, and the Form's public token in the path. The endpoint returns the judgment the ingestion endpoint would make on those answers, without making it. Pass the page's own Origin header and it is checked exactly as the submission endpoint checks it. The contract is written out in the API reference.
The response has four keys: ok, errors, checked and policy. errors is an object keyed by field name holding a list of sentences, and it stays an object even when empty so a generated caller can index it. checked names what was actually asked — shape, origin and schema — with origin absent on a Hosted page, because the page FormFlow serves is not subject to your allowlist.
Call it while the person is still on the page
One fetch is enough. This is the shape of the call; attach it to whatever event your page already trusts, such as leaving a field.
const endpoint = 'https://formflow.digital/api/v1/f/YOUR_FORM_TOKEN/preflight'
async function preflight(answers) {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify(answers),
})
// A 429 or an outage is not a verdict. Say nothing rather than something wrong.
if (!response.ok) return null
const { ok, errors, checked, policy } = await response.json()
return ok ? [] : Object.entries(errors)
}Render the sentences that come back beside the field each one names. Do not restate the rules in your own code: the point of the endpoint is that the message the person reads is the message the endpoint would have used, in the Form's language rather than your JavaScript's.
Read the answer
Key | What it holds |
|---|---|
ok | true only when nothing refused the answers |
errors | one entry per field name, each a list of sentences |
checked | what was asked: shape, origin, schema |
policy.revision | the policy version the judgment used |
policy.fingerprint | the hash identifying that exact policy |
Two keys in errors are not fields. origin carries an allowlist refusal, and submission carries a refusal that belongs to the Form rather than to one answer, such as a Form that is closed. Log the policy block: revision and fingerprint together identify the exact policy the judgment used, which is what you need on the day a pass here is followed by a refusal there.
What it does not ask
It asks only the half of the decision that is a pure function of the Form's configuration. It does not run spam scoring, does not ask the Turnstile challenge, and does not look at File fields at all — a preflight carries no bytes, so a declared image is neither checked nor promised. It stores no row, consumes no usage and delivers nothing.
It does sit inside the rate limiter. A preflight shares the token|IP bucket and the Team-wide request bucket with the submission endpoint, so a burst can return 429 with Retry-After. A 429 is the absence of a verdict, not a refusal, and the page should stay quiet rather than invent an error.
Where it fits
Every other mechanism FormFlow has for a refused Submission runs downstream of someone being turned away: the refusal ledger, the health signal, the refusal surface, the recovery inbox. This is the one that runs upstream of it. Use it as the first of two judgments, never as the only one.
A pass is not a promise. The POST decides, and it decides again with everything Preflight did not ask.
Change the Form between the two calls and the answer can change with it. The policy revision in the response is the one that judged you.
File fields are not examined, so never tell a Submitter their image was accepted on the strength of a Preflight.
errorsis keyed by field name, butoriginandsubmissionare not fields; render those at the top of the form.Anyone holding the Form's public token can call it. It proves nothing about who is calling, and it is not a secret.