Public Endpoints
These endpoints take no bearer token. They are what the official libraries call from the browser: the Signals tracker, the Forms HTML library, @snipform/react and the SnipContent runtime. Use the libraries unless you are building one; the contract below is documented so that you can.
All paths are on https://api.snipform.io. The public group is rate limited to 120 requests per minute per IP.
A form submission is a three-step session: init opens a short-lived session bound to the visitor, validate (optional) checks values without consuming it, process submits and consumes it. The full lifecycle, with what each status code means, is in How Forms Work.
Signature
Section titled “Signature”Init and the two session calls carry a signature header. It is a 32-bit hash of the UTC year, the key, and the UTC day-of-month, computed like the classic JavaScript string hash:
const signFor = (key, now = new Date()) => { const input = `${now.getUTCFullYear()}${key}${now.getUTCDate()}`; let hash = 0; for (let i = 0; i < input.length; i++) { hash = ((hash << 5) - hash + input.charCodeAt(i)) | 0; } return hash >>> 0;};For init the key is the form key; for validate and process it is the session token. The server accepts today’s or yesterday’s signature (UTC), so a form opened before midnight still submits after it.
POST /v2/form/{formKey}/init
| Header | Value |
|---|---|
X-SNIP-FORM | signFor(formKey) |
X-SNIP-LIB | Your library name and version |
X-SNIP-SID | Optional. The Signals session id, so the form hit is tied to the visitor’s session |
| Body field | Notes |
|---|---|
fields | { "fieldName": "inputType" } for every field the form will ever send. The set is frozen for the session |
validation | { "fieldName": { "rule": "message or null" } }. Parameterised rules use rule[param], for example min_length[3]. See Validation Rules |
source | Optional free-text origin label |
successContent | Optional. Your own thank-you HTML, echoed back on success with %field% variables substituted |
Managed forms ignore fields and validation and use the definition from the dashboard instead.
curl -X POST https://api.snipform.io/v2/form/YOUR_FORM_KEY/init \ -H "Content-Type: application/json" \ -H "X-SNIP-FORM: 3875461403" \ -H "X-SNIP-LIB: my-lib/1.0.0" \ -d '{ "fields": { "email": "email", "plan": "select" }, "validation": { "email": { "required": "Email is required", "email": null } } }'{ "error_type": false, "message": null, "token": "2f0e9d8c-7b6a-4f5e-8d7c-6b5a4f3e2d1c", "hp": "url", "ty": "<p>Thanks, we will be in touch.</p>", "branding": "Powered by SnipForm", "branding_link": "https://snipform.io", "branding_style": "...", "branding_style_hover": "...", "ip": "203.0.113.9", "source": "https://acme.com/contact"}| Field | Notes |
|---|---|
error_type | false on success. 1 = form key not found. 2 = configuration error (bad signature, domain not registered for the form, form not published, a field without a name, unknown validation rule) |
message | The error text when error_type is set |
token | The session id. Valid for 600 seconds, single use, pinned to the visitor’s IP |
hp | The honeypot field name. Render it hidden and submit it empty |
ty | The thank-you HTML configured on the form |
branding, branding_link | Attribution text and link, or branding: false on accounts with branding removed |
form_blueprint | Managed forms only: { fields, button, header, footer, theme, form_type, website_theme }. See Managed Forms |
Init always answers 200; read error_type.
Validate
Section titled “Validate”POST /v2/form/{token}/validate
Runs the session’s rules against the values you send and returns the errors. Nothing is saved and the session is not consumed, so a form can validate on every blur.
| Header | Value |
|---|---|
X-SNIP-SESSION | signFor(token) |
Body: { "map": { "field": "type" }, "data": { "field": "value" } }. The honeypot is stripped before validation.
{ "validated": false, "errors": { "email": ["Email is required"] } }validated: true comes with errors: null.
| Status | When |
|---|---|
| 403 | Signature does not match (Lib error: Unauthorized) |
| 419 | Unknown token, consumed, or expired (Session has expired, please refresh); or data contains a field that was not declared at init (Data discrepancy) |
Process
Section titled “Process”POST /v2/form/{token}/process
| Header | Value |
|---|---|
X-SNIP-SESSION | signFor(token) |
X-SNIP-LIB | Your library name and version; stored with the submission |
| Body field | Notes |
|---|---|
map | { "field": "type" }. Must cover every key in data |
data | { "field": "value" }. Multi-value controls send arrays. Include the honeypot field with an empty value |
ty | Optional. Overrides the thank-you HTML for this submission |
_sig | Interaction signals: { dur, gt, k, f, m, s, t, w, p, l } (milliseconds since first interaction, gate trigger, keystrokes, focus events, mouse moved, scrolled, fields touched, navigator.webdriver, plugin count, language count) |
{ "validated": true, "ty": "<p>Thanks Jane, we will be in touch.</p>",}{ "validated": false, "errors": { "email": ["Email is required"] }, "data": { "email": "", "plan": "pro" }, "map": { "email": "email", "plan": "select" }, "contentVars": { "email": "", "plan": "pro" }, "ty": "<p>Thanks, we will be in touch.</p>"}A validation failure leaves the session open; fix the values and process again. A validated submit consumes the session.
| Status | When |
|---|---|
| 403 | Signature mismatch (Lib error: Unauthorized), or the honeypot carried a value (Data invalidated) |
| 419 | Unknown token, Session has already been consumed, Session has expired, please refresh, IP switch detected, please refresh, or Data discrepancy (a key in data missing from map, or not declared at init) |
After validation passes, the _sig payload is scored. A high score rejects or silently discards the submission, but the HTTP response is identical to a clean submit. Spam never learns it was caught.
Signals tracker
Section titled “Signals tracker”Every tracker call carries X-SNIP-ID: <property key>. Responses are { "success": boolean, "message": "..." } plus the fields listed.
GET /v2/signal/load
Opens or continues the visitor’s session and records a page view.
| Header | Required | Value |
|---|---|---|
X-SNIP-ID | yes | Property key |
X-SNIP-PAGE | yes | Page URL, encodeURIComponent-ed |
X-SNIP-LIB | yes | Library version. Values from 1.1.0 up signal that page and title headers are URI-encoded |
X-SNIP-TITLE | no | Page title, URI-encoded. Empty titles are allowed |
X-SNIP-SRC | no | document.referrer, URI-encoded |
X-SNIP-STATUS | no | HTTP status of the page, default 200 |
X-SNIP-DIM | no | Viewport as WIDTHxHEIGHT |
{ "success": true, "message": "ok", "session_id": "9f1c2c1e-...", "view_id": "c1f0a6d2-..." }Missing required headers answer success: false with message: "Missing headers" and an errors array. An unknown property key answers success: false, message: "Signal ID not found".
POST /v2/signal/{viewId}/ping
Updates time on page and scroll depth for a view.
Body: { "event": "<lifecycle label, e.g. page_exit>", "tos": <seconds on page>, "max": <max scroll 0-100, or -1 if unknown> }. When sent with navigator.sendBeacon the body also carries site_id because custom headers are not possible. The server waits up to 500 ms for a view that is still being written before answering Invalid page id.
POST /v2/signal/{sessionId}/event
Body: { "event": "name", "value": <scalar or null>, "meta": { ... } }. event is required. If sessionId is stale the server rebuilds the cookieless session id from the request and uses the matching session if one exists; otherwise message: "Session not found".
Acquisition
Section titled “Acquisition”POST /v2/signal/{sessionId}/acquisition
Body: any of value (integer cents), currency_code (3 letters), tags (array of strings). Same merge semantics as the authenticated endpoint. Responds with acquisition_meta and value blocks.
Identify
Section titled “Identify”POST /v2/signal/{sessionId}/identify
Body: { "external_id": "...", "email": "...", "traits": { ... } } with at least one of external_id or email.
{ "success": true, "contact_id": "66c4d2f1a9b3c8e4f0a1b2c3", "linked_to_session": true }A browser that sends Sec-GPC: 1 gets a success-shaped no-op, { "success": true, "contact_id": null, "linked_to_session": false, "gpc": true }, and no contact is created or linked.
Resolve
Section titled “Resolve”POST /v2/signal/resolve
No body. Rebuilds the cookieless session id server-side from the request and returns { "success": true, "session_id": "..." } if a session exists for it, else { "success": false, "session_id": null }. Proxies forwarding a browser request may pass the original values as X-SF-USER-AGENT, X-SF-IP and X-SF-LANG.
Content
Section titled “Content”Render
Section titled “Render”GET /v2/content/{contentKey}/render
Serves the active version of a SnipContent block, picking the A/B version per visitor. The request’s Referer must be on the property’s base domain.
{ "success": true, "request_id": "0c9f7c5a-1d2e-4f3a-9b8c-7d6e5f4a3b2c", "content_id": "66c4...", "content_key": "hero-offer", "version_id": "66c5...", "version_label": "B", "html": "<section>...</section>", "ctas": []}Failures answer success: false with Content not found, Domain not authorized or No active version.
POST /v2/content/track
| Field | Rules |
|---|---|
request_id, content_id, version_id | required, from the render response |
action | required: view or click |
cta_key, element_key, event_name | string |
dwell_ms | integer |
meta | object |
Returns { "success": true, "interaction_id": "..." }. Rule failures return 422.
Diagnostics
Section titled “Diagnostics”Library error
Section titled “Library error”POST /v2/lib/error
Libraries report their own runtime failures here. Body: { "property_id": "...", "error": "...", "lib": "signals" | "form" }, all required. This endpoint is enveloped:
{ "code": 200, "status": "OK", "data": { "ok": true } }Invalid payloads answer 422 with { "code": 422, "status": "Error", "data": { "error": "Invalid payload" } } (or Invalid property).
Ping me
Section titled “Ping me”GET /v2/ping-me
Returns what the edge sees about the calling request: location, device, OS, browser, IP, ASN, language, bot classification and referrer. Useful for checking what a visitor’s request looks like from our side. Not for production use.