Skip to content

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.

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

HeaderValue
X-SNIP-FORMsignFor(formKey)
X-SNIP-LIBYour library name and version
X-SNIP-SIDOptional. The Signals session id, so the form hit is tied to the visitor’s session
Body fieldNotes
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
sourceOptional free-text origin label
successContentOptional. 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.

Terminal window
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 } }
}'
FieldNotes
error_typefalse 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)
messageThe error text when error_type is set
tokenThe session id. Valid for 600 seconds, single use, pinned to the visitor’s IP
hpThe honeypot field name. Render it hidden and submit it empty
tyThe thank-you HTML configured on the form
branding, branding_linkAttribution text and link, or branding: false on accounts with branding removed
form_blueprintManaged forms only: { fields, button, header, footer, theme, form_type, website_theme }. See Managed Forms

Init always answers 200; read error_type.

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.

HeaderValue
X-SNIP-SESSIONsignFor(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.

StatusWhen
403Signature does not match (Lib error: Unauthorized)
419Unknown token, consumed, or expired (Session has expired, please refresh); or data contains a field that was not declared at init (Data discrepancy)

POST /v2/form/{token}/process

HeaderValue
X-SNIP-SESSIONsignFor(token)
X-SNIP-LIBYour library name and version; stored with the submission
Body fieldNotes
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
tyOptional. Overrides the thank-you HTML for this submission
_sigInteraction 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>",
"original": { "email": "[email protected]", "plan": "pro" }
}

A validation failure leaves the session open; fix the values and process again. A validated submit consumes the session.

StatusWhen
403Signature mismatch (Lib error: Unauthorized), or the honeypot carried a value (Data invalidated)
419Unknown 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.

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.

HeaderRequiredValue
X-SNIP-IDyesProperty key
X-SNIP-PAGEyesPage URL, encodeURIComponent-ed
X-SNIP-LIByesLibrary version. Values from 1.1.0 up signal that page and title headers are URI-encoded
X-SNIP-TITLEnoPage title, URI-encoded. Empty titles are allowed
X-SNIP-SRCnodocument.referrer, URI-encoded
X-SNIP-STATUSnoHTTP status of the page, default 200
X-SNIP-DIMnoViewport 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".

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.

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.

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.

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

FieldRules
request_id, content_id, version_idrequired, from the render response
actionrequired: view or click
cta_key, element_key, event_namestring
dwell_msinteger
metaobject

Returns { "success": true, "interaction_id": "..." }. Rule failures return 422.

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).

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.