How Forms Work
Every SnipForm client, whatever it renders, runs the same conversation with https://api.snipform.io: open a short-lived form session, then submit against it. Understanding that conversation explains the constraints the clients impose and every error you can see.
The flow
Section titled “The flow”client api.snipform.io | wait for a human interaction | | POST /v2/form/{key}/init ------------> | check key, signature, domain, published, rules | <------ { token, hp, ty, branding } | create wrap session (10 min, single use, IP-pinned) | | | visitor fills the form | | POST /v2/form/{token}/validate -------> | optional, same rules, nothing consumed | <------ { validated, errors } | | | | POST /v2/form/{token}/process --------> | check session, honeypot, field set | | validate, score for spam, store, notify | <------ { validated: true, ty } | delete the session1. Declare
Section titled “1. Declare”The client collects the field names and types, and the validation rules with their messages. The HTML library reads them from your markup; the React package takes them from the fields option; managed forms have them stored on the server.
2. Wait for a human
Section titled “2. Wait for a human”Nothing is sent when the page loads. The session opens on the first sign of a person: the form scrolls into view, or gets focus, a click, a key press or a touch. The time of that first interaction is recorded and later compared with the submit time.
3. Init
Section titled “3. Init”POST /v2/form/{key}/init with the field map, the rules and the page URL, signed with the daily signature in the X-SNIP-FORM header. The server checks, in order:
| Check | Failure |
|---|---|
| The key exists | error_type: 1 |
| The signature matches today or yesterday (UTC) | error_type: 2 |
| The page’s domain is the property’s domain, or localhost with the localhost toggle on | error_type: 2 |
| The form is published | error_type: 2 |
| Every field has a name | error_type: 2 |
| Every rule name is known | error_type: 2 |
Init always answers with HTTP 200; a failure carries error_type and a message, which the clients show in place of the form. On success the server creates a wrap session holding the expected field set, the compiled rules and messages, a honeypot field name, a one-way hash of the visitor’s IP, and an expiry 10 minutes out. The response carries:
| Field | Meaning |
|---|---|
token | The session id. Every later call is addressed to it. |
hp | The honeypot field name. The client renders it hidden and sends it empty. |
ty | The thank-you HTML from the form settings. |
branding, branding_link | The “Secured by” link, or false on plans without branding. |
form_blueprint | Managed forms only: the fields to render. |
4. Submit
Section titled “4. Submit”POST /v2/form/{token}/process, signed in X-SNIP-SESSION, with the field map, the values and the behavioural signals. Before any validation the server checks the session itself:
| Condition | Response |
|---|---|
| Signature wrong or missing | 403 Lib error: Unauthorized |
| Session not found, already consumed, or older than 10 minutes | 419 |
| IP hash differs from the one at init | 419 IP switch detected |
| Honeypot field has a value | 403 Data invalidated |
| A submitted field is missing from the map, or was not declared at init | 419 Data discrepancy |
Then, in order:
- Validation. The stored rules run against the values. On failure the response is
200 { validated: false, errors: { field: [messages] } }and the session stays open for another attempt. Every message for a field is returned, not just the first. - Spam scoring. See below.
- Storage. The submission is saved, escaped. If the form restricts countries and this one is outside them, it is stored as invalid and nobody is notified.
- Side effects. Unless it scored as spam: the submit counter increments, notification emails go out, and a
form_submitevent is written to the visitor’s Signals session, which is what conversions and automations react to. - Response.
200 { validated: true, ty }, with%field%variables in the thank-you already substituted. The session is deleted.
5. Validate only
Section titled “5. Validate only”POST /v2/form/{token}/validate runs the same session checks and the same rules, and consumes nothing. It returns { validated, errors } and leaves the session exactly as it was. The React package uses it for validation on blur; the HTML library does not call it.
Consequences
Section titled “Consequences”The field set is frozen at init. Add a field to the DOM after the session opened and the submit fails with a data discrepancy. Declare everything up front, including fields you show conditionally.
Sessions are single use. The token is deleted on success. A second submit with the same token is a 419. Double clicks are harmless: the clients ignore submits while one is in flight.
Sessions last 10 minutes. A visitor who takes longer gets a 419 on submit. The HTML library opens a fresh session and the visitor submits again; the React package re-initialises and retries the submit once by itself.
Sessions are pinned to the IP hash. A network change between init and submit (Wi-Fi to mobile, a VPN connecting) is a 419 and is logged as a spam signal. Recovery is the same as expiry.
The honeypot is decided by the server. Its name is chosen per session to avoid your real fields. A filled honeypot is a hard 403, logged, with nothing stored.
No file uploads. The server accepts strings and arrays of strings only.
Spam scoring
Section titled “Spam scoring”Validation always runs first, so a real person with a typo sees the error. Only a valid submission is scored. The score adds up points from:
| Source | Signal | Points |
|---|---|---|
| Server clock | Submit under 2s / 4s / 6s / 10s after init | 50 / 30 / 15 / 5 |
Client (dur) | Under 2s / 4s between first interaction and submit | 20 / 10 |
Client (dur) | Client duration longer than the server could have seen | 10 |
Client (k) | No keystrokes / fewer than 5 | 25 / 10 |
Client (f) | No focus events / exactly one | 30 / 10 |
Client (m) | No mouse movement | 5 |
Client (t) | No fields touched | 15 |
Client (gt) | Human gate never opened | 40 |
Client (w) | navigator.webdriver is true | 50 |
Client (p) | No browser plugins | 10 |
Client (l) | No languages / exactly one | 10 / 5 |
| Network | Known datacenter ASN (AWS, GCP, Azure, DigitalOcean, Hetzner, OVH, and others) | 15 |
| Network | Tor exit node | 25 |
| Score | Action | What happens |
|---|---|---|
| under 30 | accept | Stored, notified, tracked |
| 30 to 49 | flag | Stored and tracked, marked for review in the dashboard |
| 50 to 79 | fake success | Stored as spam. No notifications, no Signals event, no automations |
| 80 and up | reject | Same as fake success, labelled as rejected |
Spam never changes the HTTP response. A bot receives the same validated: true and the same thank-you as a person, so it learns nothing about what tripped it. The difference is visible only in the dashboard, where each submission carries its score and a breakdown.
The client signals are not trusted on their own. A client that sends perfect signals is still measured against the server clock, the IP’s network, and the honeypot.
The signature
Section titled “The signature”X-SNIP-FORM and X-SNIP-SESSION carry a 32-bit hash of the UTC year, the key (or token), and the UTC day of month. The server accepts today’s and yesterday’s value so a form opened at 23:59 still submits at 00:01.
This is a handshake, not a secret: it proves the request came from a client that implements the scheme, not that it came from a browser. Protection comes from the session (single use, 10 minutes, IP-pinned, frozen field set), the domain check, the honeypot and the scoring above. If you are writing your own client, the public endpoint reference has the algorithm.
Errors at init
Section titled “Errors at init”The message shown in place of the form, and what to do:
| Message | Fix |
|---|---|
[Key error] SnipForm not found | Check the key. If your templating strips unknown attributes, use data-key. |
[Lib Error] Signature failed | The client’s clock is more than a day out, or a custom client computes the hash wrongly. |
[Config error] example.com is not a registered domain for this form | The form belongs to a property on a different domain. For local work, switch on the localhost toggle. |
[Config error] SnipForm not published | Publish the form in its settings. |
[Config error] One or more of your fields do not have a name attribute | Give every input, select and textarea a name. |
[Config error] Unknown validation rule: "requried" | Use a rule from the list. |
What is stored
Section titled “What is stored”The submitted values (HTML-escaped), the field types, the spam score and its breakdown, the client library version, and the request’s country. The visitor’s IP is stored on the wrap session only as a one-way hash and the session is deleted on submit. Read Privacy and compliance for the full picture.