Webhooks
Receive quiz events at your own URL as signed JSON, within seconds of them happening.
Webhooks send your quiz events to any URL as signed JSON, within seconds of them happening. Use them with Make, n8n, Pabbly Connect, Pipedream, or your own server. They carry the same events and data as the Zapier integration.
Available on: Starter plan and above · Platforms: Shopify and WooCommerce (identical)
| Webhooks | ShopperQuiz API | |
|---|---|---|
| Direction | ShopperQuiz pushes events to you | Your tool asks ShopperQuiz for data |
| Use it for | "When a quiz is completed, do X" | "Look up this shopper's quiz answers" |
| Setup | Paste your URL into ShopperQuiz | Paste an API key into your tool |
Events
| Event | Fires when | How often |
|---|---|---|
lead.created | A shopper enters their email on a quiz | Once per quiz attempt |
quiz.completed | A shopper reaches the results page | Once per quiz attempt |
cart.added | A shopper adds recommended products to their cart from the results | Once per quiz attempt (the first time) |
checkout.started | A shopper starts checkout from the results | Once per quiz attempt (the first time) |
order.created | A shopper who took the quiz pays for an order linked to the quiz | Once per quiz attempt (the first paid order) |
lead.created needs the quiz to ask for an email on the welcome screen. cart.added and checkout.started fire from the quiz's own buttons on the results page. For how orders are linked to a quiz, see Shopify · WooCommerce.
Add an endpoint
- Get a URL from the tool that will receive the events (see the guides below).
- In ShopperQuiz, go to Integrations → Webhooks → Add endpoint.
- Paste the URL. It must start with
https://and be reachable from the internet. - Tick the events to send. Optionally pick a single Quiz; the default is all quizzes.
- Click Add endpoint and copy the signing secret. It is shown only once. You only need it if your own code verifies requests (see below).
- Click Send test. Your tool receives a realistic sample event within a second.
You can add up to 20 endpoints. Each one can be paused or resumed with its toggle, edited, tested, given a new secret (Rotate secret), or deleted.
Guides for popular tools
Make
- Create a scenario and add Webhooks → Custom webhook. Click Add, name it, and copy the URL.
- Add that URL as an endpoint in ShopperQuiz.
- In Make, click Run once, then click Send test in ShopperQuiz. Make learns the data structure from the test.
- Add your next modules (Google Sheets, HubSpot and so on) and map fields from
data, for exampledata.shopper_emailordata.recommended_product_names. - Turn the scenario on.
n8n
- Add a Webhook node. Set HTTP Method to
POSTand copy the Production URL. - Add the URL as an endpoint in ShopperQuiz, then activate the workflow.
- Click Send test in ShopperQuiz. The event appears in the workflow's executions.
- Optional: verify the signature with a Crypto node (HMAC, SHA256) using your signing secret. See Verify the signature.
Pabbly Connect
- Create a workflow with the trigger Webhook by Pabbly and copy the webhook URL.
- Add it as an endpoint in ShopperQuiz and click Send test.
- Pabbly captures the response; map its fields in your actions.
Quick test with webhook.site
Open webhook.site, copy your unique URL, add it as an endpoint, and click Send test. You'll see the full request (headers and JSON body) instantly. Remove the endpoint when you're done.
Request format
Every event is an HTTPS POST with Content-Type: application/json:
{
"id": "0f8c2d6e-3b1a-4c7e-9f21-5a6b7c8d9e01",
"type": "quiz.completed",
"created_at": "2026-10-06T09:44:02.000Z",
"test": false,
"data": { … }
}
| Field | Description |
|---|---|
id | Unique event ID. The same on every retry, so use it to ignore duplicates |
type | One of the five event names |
created_at | When the event happened (ISO 8601, UTC) |
test | true for events sent with Send test |
data | The event's fields (below) |
Headers
| Header | Value |
|---|---|
X-ShopperQuiz-Event | The event type, for example quiz.completed |
X-ShopperQuiz-Event-Id | Same as id in the body |
X-ShopperQuiz-Delivery-Id | This delivery attempt's ID |
X-ShopperQuiz-Signature | t=<unix timestamp>,v1=<hex HMAC-SHA256> |
User-Agent | ShopperQuiz-Webhooks/1.0 (+https://shopper-quiz.com) |
Data fields
In every event
| Field | Type | Description |
|---|---|---|
response_id | string | The shopper's quiz attempt |
quiz_id, quiz_name | string | |
store_name, store_url | string | |
platform | string | shopify or woocommerce |
currency | string | Store currency. On orders, the order's currency |
shopper_email | string | Empty if the quiz didn't ask for it |
shopper_first_name, shopper_last_name, shopper_full_name | string | |
newsletter_opt_in | boolean | The shopper ticked the newsletter box |
started_at | string | When the quiz was started (ISO 8601) |
Added from quiz.completed onwards
| Field | Type | Description |
|---|---|---|
answer_<question id> | string | One per question (hyphens in the ID become underscores). Multiple answers are comma-separated; empty if skipped |
answers | array | { question_id, question, answer } for each answered question |
answers_text | string | Question: Answer, one per line |
answered_question_count | number | |
recommended_products | array | { product_id, name, price, image_url, url } |
recommended_product_names | string | Comma-separated |
recommended_product_count | number | |
top_recommended_product_name, top_recommended_product_url | string | |
filters_matched, filters_total | number |
product_id is your store's product ID (Shopify or WooCommerce).
Added on cart.added and checkout.started
| Field | Type | Description |
|---|---|---|
cart_items | array | { product_id, name, price, quantity, image_url, url, subscription, variant }: subscription is the plan name for an item the shopper chose to subscribe to, otherwise empty. variant is the chosen option (e.g. Red / M) for a product with several options, otherwise empty. A shopper can add several options of one product, so the same product_id can appear on more than one line |
cart_value | number | Sum of price × quantity |
cart_item_count | number | Total quantity |
cart_product_names | string | Comma-separated |
cart_has_subscription | boolean | At least one item is a subscription |
Added on order.created
| Field | Type | Description |
|---|---|---|
order_id | string | Shopify or WooCommerce order ID |
order_number | string | For example #1042 |
order_total | number | |
order_items | array | { product_id, name, price, quantity, subscription } — subscription is the plan name for a line bought on a subscription, otherwise empty |
order_item_count | number | |
order_product_names | string | |
includes_recommended_product | boolean | The shopper bought something the quiz recommended |
order_has_subscription | boolean | At least one line was bought on a subscription |
Full example: order.created
{
"id": "c73f72e4-fbb5-47ae-96f7-6d7ef2403584",
"type": "order.created",
"created_at": "2026-10-06T07:16:23.000Z",
"test": false,
"data": {
"response_id": "7d1e4f2a-9c3b-4e8d-a6f5-1b2c3d4e5f60",
"quiz_id": "3a9b8c7d-6e5f-4a3b-9c2d-1e0f9a8b7c6d",
"quiz_name": "Hair Discovery Quiz",
"store_name": "ARTINIQ",
"store_url": "https://artiniq.myshopify.com",
"platform": "shopify",
"currency": "AUD",
"shopper_email": "jane.cooper@example.com",
"shopper_first_name": "Jane",
"shopper_last_name": "Cooper",
"shopper_full_name": "Jane Cooper",
"newsletter_opt_in": true,
"started_at": "2026-10-06T07:09:12.000Z",
"answer_b1c2d3e4_0000_4000_8000_000000000001": "Curly",
"answer_b1c2d3e4_0000_4000_8000_000000000002": "Dry, Frizzy",
"answers": [
{ "question_id": "b1c2d3e4-0000-4000-8000-000000000001", "question": "What's your hair type?", "answer": "Curly" },
{ "question_id": "b1c2d3e4-0000-4000-8000-000000000002", "question": "What's your hair condition?", "answer": "Dry, Frizzy" }
],
"answers_text": "What's your hair type?: Curly\nWhat's your hair condition?: Dry, Frizzy",
"answered_question_count": 2,
"recommended_products": [
{ "product_id": "8012345678901", "name": "Amla Powder", "price": 40, "image_url": "https://cdn.shopify.com/…/amla-powder.jpg", "url": "https://artiniq.myshopify.com/products/amla-powder" },
{ "product_id": "8012345678902", "name": "Jamun Seed Oil", "price": 42, "image_url": "https://cdn.shopify.com/…/jamun-seed-oil.jpg", "url": "https://artiniq.myshopify.com/products/jamun-seed-oil" }
],
"recommended_product_names": "Amla Powder, Jamun Seed Oil",
"recommended_product_count": 2,
"top_recommended_product_name": "Amla Powder",
"top_recommended_product_url": "https://artiniq.myshopify.com/products/amla-powder",
"filters_matched": 1,
"filters_total": 1,
"order_id": "5123456789012",
"order_number": "#1042",
"order_total": 82,
"order_items": [
{ "product_id": "8012345678901", "name": "Amla Powder", "price": 40, "quantity": 1, "subscription": "" },
{ "product_id": "8012345678902", "name": "Jamun Seed Oil", "price": 42, "quantity": 1, "subscription": "" }
],
"order_item_count": 2,
"order_product_names": "Amla Powder, Jamun Seed Oil",
"includes_recommended_product": true,
"order_has_subscription": false
}
}
A lead.created event contains only the fields listed under In every event.
Verify the signature
If your own code receives webhooks, verify each request with the endpoint's signing secret:
- Read
tandv1from theX-ShopperQuiz-Signatureheader. - Compute HMAC-SHA256 of
t + "." + <raw request body>with your signing secret, as lowercase hex. - Compare it to
v1with a constant-time comparison. - Reject requests where
tis more than 5 minutes from now.
Always use the raw body bytes, before any JSON parsing.
Node.js
const crypto = require('crypto')
function verifyShopperQuiz(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')))
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
const expected = crypto.createHmac('sha256', secret).update(parts.t + '.' + rawBody).digest('hex')
return parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
}
Python
import hmac, hashlib, time
def verify_shopperquiz(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
PHP
function verify_shopperquiz(string $rawBody, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $parts);
if (abs(time() - (int) $parts['t']) > 300) return false;
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
To change a secret, click Rotate secret. The old secret stops working immediately, so update your code at the same time.
Responding, retries and duplicates
- Respond with any 2xx status within 10 seconds. Do slow work after responding.
- Any other status, a timeout or a connection error is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then marked Failed. You can Retry now from Recent deliveries.
- Respond 410 Gone to turn the endpoint off permanently.
- After 50 failed deliveries in a row, the endpoint is paused automatically and a notice appears in the drawer. Fix it, then switch it back on.
- Redirects are not followed. Use the final URL.
- Events can arrive more than once (for example, when a retry follows a slow response) and not always in order. Use
idto ignore duplicates, andcreated_atto order them.
Timing and limits
- Events are sent about 15 to 30 seconds after they happen.
- Up to 20 endpoints per store. Delivery history is kept for 30 days.
- Requests come from ShopperQuiz's servers. There is no fixed IP address to allowlist; verify the signature instead.
Security
- Endpoints must use HTTPS and point to a public address. Private, local and cloud-metadata addresses are refused.
- Each endpoint has its own signing secret. It is kept server-side, never readable from the dashboard, and shown only once when created or rotated.
- Use Sync per quiz in the drawer to keep any quiz out of webhooks.
Was this page helpful?
Need more help? Open Ask Ana AI in your dashboard, or email support@shopper-quiz.com.