Skip to main content

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)

WebhooksShopperQuiz API
DirectionShopperQuiz pushes events to youYour tool asks ShopperQuiz for data
Use it for"When a quiz is completed, do X""Look up this shopper's quiz answers"
SetupPaste your URL into ShopperQuizPaste an API key into your tool

Events​

EventFires whenHow often
lead.createdA shopper enters their email on a quizOnce per quiz attempt
quiz.completedA shopper reaches the results pageOnce per quiz attempt
cart.addedA shopper adds recommended products to their cart from the resultsOnce per quiz attempt (the first time)
checkout.startedA shopper starts checkout from the resultsOnce per quiz attempt (the first time)
order.createdA shopper who took the quiz pays for an order linked to the quizOnce 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​

  1. Get a URL from the tool that will receive the events (see the guides below).
  2. In ShopperQuiz, go to Integrations → Webhooks → Add endpoint.
  3. Paste the URL. It must start with https:// and be reachable from the internet.
  4. Tick the events to send. Optionally pick a single Quiz; the default is all quizzes.
  5. 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).
  6. 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.

Make​

  1. Create a scenario and add Webhooks → Custom webhook. Click Add, name it, and copy the URL.
  2. Add that URL as an endpoint in ShopperQuiz.
  3. In Make, click Run once, then click Send test in ShopperQuiz. Make learns the data structure from the test.
  4. Add your next modules (Google Sheets, HubSpot and so on) and map fields from data, for example data.shopper_email or data.recommended_product_names.
  5. Turn the scenario on.

n8n​

  1. Add a Webhook node. Set HTTP Method to POST and copy the Production URL.
  2. Add the URL as an endpoint in ShopperQuiz, then activate the workflow.
  3. Click Send test in ShopperQuiz. The event appears in the workflow's executions.
  4. Optional: verify the signature with a Crypto node (HMAC, SHA256) using your signing secret. See Verify the signature.

Pabbly Connect​

  1. Create a workflow with the trigger Webhook by Pabbly and copy the webhook URL.
  2. Add it as an endpoint in ShopperQuiz and click Send test.
  3. 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": { … }
}
FieldDescription
idUnique event ID. The same on every retry, so use it to ignore duplicates
typeOne of the five event names
created_atWhen the event happened (ISO 8601, UTC)
testtrue for events sent with Send test
dataThe event's fields (below)

Headers​

HeaderValue
X-ShopperQuiz-EventThe event type, for example quiz.completed
X-ShopperQuiz-Event-IdSame as id in the body
X-ShopperQuiz-Delivery-IdThis delivery attempt's ID
X-ShopperQuiz-Signaturet=<unix timestamp>,v1=<hex HMAC-SHA256>
User-AgentShopperQuiz-Webhooks/1.0 (+https://shopper-quiz.com)

Data fields​

In every event​

FieldTypeDescription
response_idstringThe shopper's quiz attempt
quiz_id, quiz_namestring
store_name, store_urlstring
platformstringshopify or woocommerce
currencystringStore currency. On orders, the order's currency
shopper_emailstringEmpty if the quiz didn't ask for it
shopper_first_name, shopper_last_name, shopper_full_namestring
newsletter_opt_inbooleanThe shopper ticked the newsletter box
started_atstringWhen the quiz was started (ISO 8601)

Added from quiz.completed onwards​

FieldTypeDescription
answer_<question id>stringOne per question (hyphens in the ID become underscores). Multiple answers are comma-separated; empty if skipped
answersarray{ question_id, question, answer } for each answered question
answers_textstringQuestion: Answer, one per line
answered_question_countnumber
recommended_productsarray{ product_id, name, price, image_url, url }
recommended_product_namesstringComma-separated
recommended_product_countnumber
top_recommended_product_name, top_recommended_product_urlstring
filters_matched, filters_totalnumber

product_id is your store's product ID (Shopify or WooCommerce).

Added on cart.added and checkout.started​

FieldTypeDescription
cart_itemsarray{ 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_valuenumberSum of price × quantity
cart_item_countnumberTotal quantity
cart_product_namesstringComma-separated
cart_has_subscriptionbooleanAt least one item is a subscription

Added on order.created​

FieldTypeDescription
order_idstringShopify or WooCommerce order ID
order_numberstringFor example #1042
order_totalnumber
order_itemsarray{ product_id, name, price, quantity, subscription } — subscription is the plan name for a line bought on a subscription, otherwise empty
order_item_countnumber
order_product_namesstring
includes_recommended_productbooleanThe shopper bought something the quiz recommended
order_has_subscriptionbooleanAt 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:

  1. Read t and v1 from the X-ShopperQuiz-Signature header.
  2. Compute HMAC-SHA256 of t + "." + <raw request body> with your signing secret, as lowercase hex.
  3. Compare it to v1 with a constant-time comparison.
  4. Reject requests where t is 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 id to ignore duplicates, and created_at to 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.