Skip to content

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.

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 session

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.

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.

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:

CheckFailure
The key existserror_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 onerror_type: 2
The form is publishederror_type: 2
Every field has a nameerror_type: 2
Every rule name is knownerror_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:

FieldMeaning
tokenThe session id. Every later call is addressed to it.
hpThe honeypot field name. The client renders it hidden and sends it empty.
tyThe thank-you HTML from the form settings.
branding, branding_linkThe “Secured by” link, or false on plans without branding.
form_blueprintManaged forms only: the fields to render.

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:

ConditionResponse
Signature wrong or missing403 Lib error: Unauthorized
Session not found, already consumed, or older than 10 minutes419
IP hash differs from the one at init419 IP switch detected
Honeypot field has a value403 Data invalidated
A submitted field is missing from the map, or was not declared at init419 Data discrepancy

Then, in order:

  1. 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.
  2. Spam scoring. See below.
  3. 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.
  4. Side effects. Unless it scored as spam: the submit counter increments, notification emails go out, and a form_submit event is written to the visitor’s Signals session, which is what conversions and automations react to.
  5. Response. 200 { validated: true, ty }, with %field% variables in the thank-you already substituted. The session is deleted.

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.

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.

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:

SourceSignalPoints
Server clockSubmit under 2s / 4s / 6s / 10s after init50 / 30 / 15 / 5
Client (dur)Under 2s / 4s between first interaction and submit20 / 10
Client (dur)Client duration longer than the server could have seen10
Client (k)No keystrokes / fewer than 525 / 10
Client (f)No focus events / exactly one30 / 10
Client (m)No mouse movement5
Client (t)No fields touched15
Client (gt)Human gate never opened40
Client (w)navigator.webdriver is true50
Client (p)No browser plugins10
Client (l)No languages / exactly one10 / 5
NetworkKnown datacenter ASN (AWS, GCP, Azure, DigitalOcean, Hetzner, OVH, and others)15
NetworkTor exit node25
ScoreActionWhat happens
under 30acceptStored, notified, tracked
30 to 49flagStored and tracked, marked for review in the dashboard
50 to 79fake successStored as spam. No notifications, no Signals event, no automations
80 and uprejectSame 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.

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.

The message shown in place of the form, and what to do:

MessageFix
[Key error] SnipForm not foundCheck the key. If your templating strips unknown attributes, use data-key.
[Lib Error] Signature failedThe 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 formThe form belongs to a property on a different domain. For local work, switch on the localhost toggle.
[Config error] SnipForm not publishedPublish the form in its settings.
[Config error] One or more of your fields do not have a name attributeGive every input, select and textarea a name.
[Config error] Unknown validation rule: "requried"Use a rule from the list.

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.