Quizproof

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#

  1. Go to Settings › Integrations › Webhooks.

  2. 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.").

  3. Choose the events it receives, then Add endpoint.

  4. Copy the signing secret (whsec_…). It's shown once. Use it to check that a delivery comes from Quizproof.

  5. 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 }
    }
  }
}

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):

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.

  1. In Zapier, create a Zap. Trigger: Webhooks by Zapier, event Catch Hook. Copy the webhook URL Zapier shows.

  2. In Quizproof, add an endpoint with that URL, choose the events, and Send test event.

  3. 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.

  4. Add your action, for example a Mailchimp "Add/Update Subscriber" with data contact email. For marketing, add a Filter step: data contact consents marketing_email is true, so only shoppers who agreed are added.

  5. Turn the Zap on. Test events have test true: 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#

Secrets, turning off and deleting#

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