Registration payload reference
Pure spec for the Registration webhook body. For overview, signature verification, and retry behaviour see Registration webhook.
Top-level fields#
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | Email the visitor submitted at the registration step. |
answers | object | yes | Map of Component ID → answer object. See Answers and question types. |
context | object | yes | Session metadata. See Context fields. |
Context fields#
| Field | Type | Required | Description |
|---|---|---|---|
appUserId | string (UUID) | yes | Zellify funnel session identifier. Same value appears in Stripe, Paddle, and Solidgate metadata as app_user_id. |
funnelId | string | yes | Funnel where the registration happened. |
campaignId | string | yes | Campaign link used to reach the funnel. |
experimentId | string | null | yes | Experiment id if the campaign targets one, else null. |
experimentTag | string | null | yes | Alias of experimentId — both fields carry the same value. Kept for backwards compatibility; prefer experimentId in new code. |
organizationId | string (UUID) | yes | Your Zellify organization id. |
pageLocation | string | yes | Path where the registration event fired. |
ip | string | yes | Visitor IP (v4 or v6). |
userAgent | string | yes | Visitor user agent string. |
sessionToken | string | null | yes | Opaque session continuity token used internally by Zellify's tracker. Treat as opaque; not typically needed for integrations. |
Answers and question types#
answers is keyed by the Component ID set on each question component in the funnel builder (e.g. age, gender). Each entry has this shape:
1type Answer<V> = {2 value: V // V varies by question type — see table below3 label?: string // chosen option's display text; for multiselect, a comma-joined string of selected labels4 pageSlug?: string // slug of the page the question lives on5 questionText?: string // the question's prompt as configured in the builder6}
Only value is guaranteed. The three metadata fields appear when populated by the question component (label requires options metadata; pageSlug requires the variable to be declared on a page; questionText requires the component author to set it). Plain email/text/number inputs typically only emit value and pageSlug.
| Question type | value shape | Example |
|---|---|---|
email | string | { "value": "[email protected]" } |
text | string | { "value": "free text" } |
number | number | { "value": 35 } |
singleselect | string (option ID) | { "value": "2", "label": "Female" } |
multiselect | string[] (option IDs) | { "value": ["1", "3"], "label": "Apple, Banana" } |
slider | string (option ID) | { "value": "3", "label": "Often" } |
range | string ("from-to") | { "value": "18-35" } |
Option IDs are sequential numeric strings ("1", "2", "3", …) auto-assigned by the builder. They're stable across edits — deleting an option doesn't renumber the others, and new options get the next available ID. For singleselect and slider, label carries the option's display text directly so you usually don't need to resolve IDs at all. For multiselect, label joins the chosen option labels with ", ". If you need an array of labels, a per-option mapping, or the question's full schema for type generation, export the Variables Schema below.
range answers serialize as a single string in "from-to" format (e.g. "18-35"); split on the hyphen to read the bounds.
Variables Schema#
The webhook payload carries raw user input. The Variables Schema gives you the metadata to interpret it — every question in the funnel, its type, its prompt, the page it lives on, and (for choice questions) the full set of options.
You'll want it any time you:
- Generate TypeScript types or a JSON schema for the answer payload your backend sees
- Render a per-option report (e.g. "5 users picked Apple, 12 picked Banana")
- Validate or normalise answers server-side beyond what
labelalready gives you - Build a dashboard, CRM, or analytics view that knows what every question was
How to export#
- Open Dashboard → Funnels
- Click Edit Funnel on the funnel you want
- Open the Variables tab in the right sidebar
- Click Copy Variables Schema (top-right of the panel)
- Paste into your backend repo, design doc, or schema-generation pipeline
The export is a JSON array — one entry per declared variable in the funnel.
Schema shape#
Each entry describes one question:
1type SchemaVariable = {2 name: string // the Component ID — same key used in `answers`3 questionType:4 | "email" | "text" | "number"5 | "singleselect" | "multiselect"6 | "slider" | "range"7 questionText: string // the question prompt as shown to the user8 pageSlug: string // the page the question lives on, "" if unbound9 options?: Array<{ // present for singleselect, multiselect, slider10 id: string // matches the `value` you'll see in `answers`11 label?: string // display text — same value the runtime puts in `label`12 }>13 range?: { from: number; to: number; step: number } // present for range / numeric wheel pickers14}
Example#
1[2 {3 "name": "nickname",4 "questionType": "text",5 "questionText": "What is your nickname",6 "pageSlug": ""7 },8 {9 "name": "email",10 "questionType": "email",11 "questionText": "Enter your email to get your personal plan",12 "pageSlug": "page-2"13 },14 {15 "name": "age",16 "questionType": "number",17 "questionText": "What is your age",18 "pageSlug": ""19 },20 {21 "name": "gender",22 "questionType": "singleselect",23 "questionText": "What is your gender",24 "pageSlug": "",25 "options": [26 { "id": "1", "label": "Male" },27 { "id": "2", "label": "Female" }28 ]29 },30 {31 "name": "favoriteActivities",32 "questionType": "multiselect",33 "questionText": "What is your favorite activity",34 "pageSlug": "",35 "options": [36 { "id": "1", "label": "Play" },37 { "id": "2", "label": "Dance" },38 { "id": "3", "label": "Paint" },39 { "id": "4", "label": "Walk" }40 ]41 }42]
The name of each schema entry matches the answers key in the webhook payload, and each option's id matches what you'll see as value (or as an entry inside value for multiselect). That's the contract — schema and payload key off the same identifiers.
When you don't need it#
If your handler only needs app_user_id plus a few well-known fields (email, age, etc.), and you're happy reading label directly off the payload for choice questions, you can skip the schema. The runtime payload is self-describing for the common cases — schema export becomes important when you need the full picture (every question, every option, every type) ahead of time.
Handling missing fields#
Every required field in the tables above is always present on a valid, signature-verified payload. Treat a missing required field as a sign the body was tampered with or the request is not from Zellify, and reject with 400.
The optional answer fields (label, pageSlug, questionText) may legitimately be absent — handle the missing case rather than assuming they're present.