API Reference
Webhooks
Payload schemas, signature verification, and delivery behavior for webhooks SuperFunnel sends to your endpoint.
Webhooks are the one part of the platform where SuperFunnel calls you. When a visitor submits a form, fills one out partially, or interacts with a page, SuperFunnel sends an HTTP request to an endpoint you own.
This page documents what arrives at that endpoint, and how to confirm the request genuinely came from SuperFunnel.
Setting a webhook up
Creating a webhook, choosing its event types, and configuring method, content type, and delivery filters all happen in the dashboard. See the Webhooks guide for that walkthrough.
Defaults: a POST with a JSON body. The method and content type are both
configurable per webhook, so the schemas below describe the default JSON
encoding.
Webhook types
Each webhook subscribes to exactly one type, chosen when you create it. The
type field on the root of every payload tells you which one fired.
| Type | Trigger |
|---|---|
new_lead | A visitor completes and submits a form. |
new_partial_lead | Sent incrementally as a visitor fills out a form, before they submit. |
new_event | A visitor interaction on the page, filtered by the event types you pick. |
For new_event, you choose which interactions to receive. The available event
types are view, conversion, scroll, button-click, form-submission,
ping, scheduled-event, user-error, popup-view, popup-click, and
popup-close (or all of them).
Authentication
Secure your webhook with one of two methods, selected when you create it.
Method A: API key (recommended)
You define a static key in the SuperFunnel UI. It is sent in the HTTP
Authorization header on every request, exactly as you entered it:
Authorization: <your-custom-api-key>Reject any request whose header does not match your key.
Method B: Public/private key
SuperFunnel generates an RSA key pair and signs every request, placing a token
in the Authorization header. Download the public key from the integration's
settings and use it to verify that token before trusting the payload. The
token is a JWT signed with RS256, carries the claim iss: "superfunnel.ai",
and expires one hour after it is issued.
import jwt from 'jsonwebtoken'
// publicKey: the PEM you downloaded from the webhook's settings.
// token: the raw value of the Authorization header (no "Bearer " prefix).
function verifyRequest(token: string, publicKey: string) {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'superfunnel.ai',
}) // throws if the signature is invalid or the token has expired
}Delivery behavior
- Retry policy — if your endpoint does not return a
200 OK, SuperFunnel retries the delivery up to 3 times with exponential backoff. - Timeout — respond quickly so a slow handler doesn't trigger a retry. Acknowledge with a
200first and do the real work asynchronously. - At-least-once delivery — a retried delivery repeats a payload your endpoint may have already processed. Deduplicate on
data.id, which is stable across retries of the same event. - Object mapping — by default you receive the full payloads documented below. If object mapping rules are defined on the webhook, the default payload is discarded and your endpoint receives only the structure defined there.
- Per-field encoding — mapped fields carry an encoding, set per row and defaulting to none. A field may arrive Base64, URL-safe Base64, or as a JSON string, in which case your handler decodes it before use. Encoding is a property of the mapping, not the payload, so nothing in the request identifies which fields were encoded. Read the webhook's configuration to know what to expect.
Partial leads fire repeatedly
A new_partial_lead webhook sends a payload at every step of the form, not
just on completion. Either configure a
delivery filter so SuperFunnel
only sends payloads containing a key field, or gate your handler on that field
yourself, so half-finished records don't pile up downstream.
Payload format
Every default delivery has a JSON body whose root contains a type field and a
data object.
New lead
new_lead fires when a visitor completes and submits a form. Form answers live
under data.form, keyed by field with a value and label. createdAt is a
Firestore timestamp (_seconds / _nanoseconds).
{
"type": "new_lead",
"data": {
"accountId": "0RtsITKvLklzoTXqSZaO",
"campaignId": "UubgLfCPScWvTXN3hSkI",
"variantId": "bA3YhT2gLXzOCyo7qqeA",
"websiteId": "CuRMjNPCe4WCVOOITqru",
"formId": "JYgrG3oeO8GLgKlBPas9",
"formName": "Lead capture form",
"submissionId": "TkDAGQktTGHf",
"form": {
"name": { "value": "Jane Doe", "label": "Name" },
"email": { "value": "jane@example.com", "label": "Email" },
"phone": { "value": "+15555550123", "label": "Phone" },
"message": { "value": "", "label": "Message" }
},
"url": "https://yourbrand.superfunnel.page/landing?utm=spring_sale",
"queryParams": { "utm": "spring_sale" },
"ip": "203.0.113.42",
"userHash": 6240556724501698,
"sessionHash": 3613007597330076,
"visitHash": 8079222503494134,
"id": "6jXpAC82AANQ0GLSq8d5",
"createdAt": { "_seconds": 1767621070, "_nanoseconds": 212000000 }
}
}Partial lead
new_partial_lead is sent repeatedly as the visitor fills out the form, so most
payloads are incomplete. It adds currentStep and lastUpdatedAt, and its id
is {sessionHash}_{submissionId} rather than a standalone document ID. Upsert
on that id to keep one row per in-progress submission.
{
"type": "new_partial_lead",
"data": {
"accountId": "0RtsITKvLklzoTXqSZaO",
"campaignId": "UubgLfCPScWvTXN3hSkI",
"variantId": "bA3YhT2gLXzOCyo7qqeA",
"websiteId": "CuRMjNPCe4WCVOOITqru",
"formId": "1Ts6JMftPUTsOdOMTPWC",
"formName": "Qualify for Your Debt Consolidation Loan",
"submissionId": "dpke3fVx1gLP",
"currentStep": 0,
"form": {
"name": { "value": "Jane Doe", "label": "Name" },
"email": { "value": "jane@example.com", "label": "Email" }
},
"url": "https://yourbrand.superfunnel.page/landing?utm=spring_sale",
"queryParams": { "utm": "spring_sale" },
"userHash": 6240556724501698,
"sessionHash": 3613007597330076,
"visitHash": 5291661038735089,
"id": "3613007597330076_dpke3fVx1gLP",
"lastUpdatedAt": { "_seconds": 1767621954, "_nanoseconds": 474000000 },
"createdAt": { "_seconds": 1767621954, "_nanoseconds": 474000000 }
}
}Page events
new_event is fired by visitor interactions. The event field names the
interaction, and the rest carries full visitor context (device, location,
referrer, and session hashes). A scroll payload is identical to a view but
with "event": "scroll".
{
"type": "new_event",
"data": {
"id": "59176294-00fd-42e2-8003-4afe85421adb",
"event": "view",
"createdOn": "2026-01-05T13:49:19.616Z",
"createdOnUnix": 1767620959616,
"accountId": "0RtsITKvLklzoTXqSZaO",
"campaignId": "UubgLfCPScWvTXN3hSkI",
"variantId": "bA3YhT2gLXzOCyo7qqeA",
"websiteId": "CuRMjNPCe4WCVOOITqru",
"path": "/landing",
"search": "?utm=spring_sale",
"title": "Home",
"url": "https://yourbrand.superfunnel.page",
"queryParams": { "utm": "spring_sale" },
"ip": "203.0.113.42",
"clientIp": "203.0.113.42",
"device": "Desktop",
"os": "Mac OS",
"browser": "Chrome",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
"origin": "https://yourbrand.superfunnel.page",
"referrer": "https://app.superfunnel.ai/",
"referrerDomain": "app.superfunnel.ai",
"adNetworkProvider": "unknown",
"country": "US",
"region": "CA",
"city": "San Francisco",
"isBot": false,
"hash": 6240556724501698,
"sessionHash": 3613007597330076,
"visitHash": 8079222503494134,
"shortHash": "sA4v2N2Fk",
"shortSessionHash": "gxX1xyTeY",
"shortVisitHash": "B0bf3xjzE"
}
}A button-click event includes the same fields plus an extra object with DOM
and element details:
"extra": {
"rawHTML": true,
"buttonId": "3c68e6bc",
"buttonPath": "section#section-html-v1 > div > button.inline-flex",
"sectionId": "99bbc91cf844d660",
"sectionType": "html",
"sectionIndex": 0,
"buttonType": "form",
"buttonLink": null
}Session identifiers
sessionHash, visitHash, and userHash appear on every payload and are the
same identifiers the conversions endpoint matches on.
Storing them alongside a lead is what lets you report a backend conversion later
and attribute it to the visit that produced it.