Webhooks: payload and signature
A webhook sends each quiz event to an HTTPS address you choose, as it happens: a Zapier "Catch Hook", a Make or n8n webhook, or your own server. Use it to reach a tool Quizproof has no direct connection to. It's available on every plan.
Add an endpoint#
Go to Settings › Integrations › Webhooks.
Paste the endpoint's URL. It must be an
https://address on the internet; Quizproof refuses private and local addresses ("This address isn't reachable from the internet.").Choose the events it receives, then Add endpoint.
Copy the signing secret (
whsec_…). It's shown once. Use it to check that a delivery comes from Quizproof.Use Send test event to send a sample of each event, marked
"test": true, with a real signature.
A store can have up to 5 endpoints.
The events#
quiz.completed: a shopper finished a quiz and saw their recommendations. It carries the quiz, the recommended products and a link to the results page. No shopper details and no answers.
optin.submitted: a shopper who finished the quiz left their email and agreed to at least one thing (email marketing, text messages or their results by email). It adds their email, phone, Shopify customer id, consents and saved answers (the answers of questions set to save to the customer). Sent once per shopper and quiz session; a shopper who corrects their address is a new event.
Every event looks like this (an optin.submitted):
{
"v": 1,
"id": "evt_01K7GQ2ZB8X4N6R0S3T5V7W9YB",
"type": "optin.submitted",
"created_at": "2026-10-14T09:30:00Z",
"shop": "your-store.myshopify.com",
"test": false,
"data": {
"quiz": { "handle": "skin-quiz", "name": "Skin quiz", "version": 3 },
"session_id": "0b9a8f52-3c1e-4d7a-9a51-6a2b7f0c1d23",
"market_country": "DE",
"result_url": "https://your-store.com/apps/quizproof/r/…",
"products": [
{
"slot": "cleanser",
"product_id": "8012345678901",
"variant_id": "45012345678901",
"title": "Gentle Cream Cleanser",
"url": "https://your-store.com/products/gentle-cream-cleanser",
"image": "https://cdn.shopify.com/s/files/…/cleanser.png",
"price": "18.00",
"currency": "EUR",
"available": true
}
],
"answers": [{ "question": "What's your skin type?", "answers": ["Dry"] }],
"contact": {
"email": "shopper@example.com",
"phone": "+4915123456789",
"customer_id": "7012345678901",
"consents": { "marketing_email": true, "marketing_sms": true, "results_email": false }
}
}
}
ididentifies the event; the same value is in thewebhook-idheader.Ids are numeric strings.
priceis a decimal string in the shopper's market currency, or""when Quizproof doesn't know it.result_urlis a link to the shopper's results page (valid 7 days), ornull.phoneandcustomer_idarenullwhen the shopper gave no phone, or Shopify hasn't saved them as a customer yet.quiz.completed has the same fields without
answersandcontact.New fields may be added. Existing ones keep their meaning.
Duplicates and order#
Delivery is "at least once": an event can arrive more than once (for example when your endpoint answered slowly and
Quizproof retried). Skip one you've already handled by its webhook-id. Events can also arrive out of order.
Verify the signature#
Each delivery carries three headers (Standard Webhooks):
webhook-id: the event's id.webhook-timestamp: when this attempt was sent (seconds since 1970).webhook-signature:v1,and a base64 HMAC-SHA256 ofwebhook-id.webhook-timestamp.bodywith your secret (the part afterwhsec_, base64-decoded). Right after you rotate the secret it holds two signatures, separated by a space: accept the delivery when one of them matches.
Check the signature against the raw body, before parsing it, and refuse a timestamp older than 5 minutes. The Standard
Webhooks libraries (standardwebhooks for Node, Python and Go) do all of this.
Node:
import crypto from "node:crypto";
function verify(secret, headers, rawBody) {
const id = headers["webhook-id"], ts = headers["webhook-timestamp"], sigs = headers["webhook-signature"];
if (!id || !ts || !sigs || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const want = "v1," + crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64");
return sigs.split(" ").some((s) => s.length === want.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(want)));
}
Python:
import base64, hashlib, hmac, time
def verify(secret: str, headers, raw_body: bytes) -> bool:
msg_id, ts, sigs = headers.get("webhook-id"), headers.get("webhook-timestamp"), headers.get("webhook-signature")
if not (msg_id and ts and sigs) or abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
mac = hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()
want = "v1," + base64.b64encode(mac).decode()
return any(hmac.compare_digest(s, want) for s in sigs.split(" "))
Go:
func verify(secret string, h http.Header, body []byte) bool {
id, ts, sigs := h.Get("webhook-id"), h.Get("webhook-timestamp"), h.Get("webhook-signature")
t, err := strconv.ParseInt(ts, 10, 64)
if id == "" || err != nil || time.Since(time.Unix(t, 0)).Abs() > 5*time.Minute {
return false
}
key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
if err != nil {
return false
}
mac := hmac.New(sha256.New, key)
mac.Write([]byte(id + "." + ts + "."))
mac.Write(body)
want := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))
for _, s := range strings.Fields(sigs) {
if hmac.Equal([]byte(s), []byte(want)) {
return true
}
}
return false
}
Zapier: Catch Hook#
The Quizproof Zapier app is simpler and works on Zapier's free plan: see connect Quizproof to Zapier. Webhooks by Zapier, below, is part of Zapier's paid plans.
In Zapier, create a Zap. Trigger: Webhooks by Zapier, event Catch Hook. Copy the webhook URL Zapier shows.
In Quizproof, add an endpoint with that URL, choose the events, and Send test event.
Back in Zapier, Test trigger: it finds the test event. Its fields (
data contact email,data products 1 title, …) can be mapped in the next steps.Add your action, for example a Mailchimp "Add/Update Subscriber" with
data contact email. For marketing, add a Filter step:data contact consents marketing_emailis true, so only shoppers who agreed are added.Turn the Zap on. Test events have
testtrue: filter them out if your action shouldn't see them.
Catch Hook doesn't check the signature. To verify it, use Catch Raw Hook and a Code step with the Node snippet above.
Retries and failures#
Any 2xx answer means delivered. Quizproof reads at most 4 KB of the answer and keeps none of it.
No answer within 10 seconds, a network error, a redirect or another status: Quizproof tries again after 1, 2, 5, 15 and 30 minutes, then 1, 2, 4 and 6 hours (10 attempts, about 14 hours), with the same body and
webhook-idand a new timestamp. After that the delivery is failed.410 Gone turns the endpoint off at once (shown on the page). Enable it again when it's ready.
After 3 failed deliveries in a row the endpoint shows as failing on the page and on Home; after 10 Quizproof emails your alert address once; after 25 it turns the endpoint off. One delivered event clears it.
Retry failed sends the failed deliveries of the last 7 days again, unchanged.
The page lists each endpoint's last 50 requests: time, event,
webhook-id, attempt, HTTP status, time taken and the error.
Secrets, turning off and deleting#
Rotate secret makes a new secret. The old one keeps signing too for 24 hours, so you can switch without missing a delivery. You can rotate again once those 24 hours are over.
Disable stops deliveries and cancels the ones waiting to retry. Enable starts them again for new events.
Delete removes the endpoint and its secret, and cancels its waiting deliveries.
Privacy#
Opt-in events carry the shopper's personal data to the endpoint you chose; what happens to it there is up to you (you're the controller). Quizproof cancels a waiting opt-in delivery when the shopper withdraws every consent, and cancels and deletes waiting deliveries when a customer asks to be forgotten. Deliveries already made aren't recalled: delete the data in your own tool when a customer asks.
Menu paths such as Settings › Privacy are inside Quizproof: in your Shopify admin, open Apps › Quizproof. Where an article means Shopify's own settings, it says so.
Something unclear? Email support@quizproof.app. We aim to reply within 1 business day.
More in Integrations: Klaviyo setup, Collect emails with the opt-in question, Omnisend: what Quizproof sends, Connect Quizproof to Zapier, Use quiz answers in customer segments, Apps that work with Quizproof