Guide to Zigpoll Webhooks: Features and Best Practices
Zigpoll webhooks send survey answers to your own systems as respondents submit them. If you run on-site or Shopify surveys with Zigpoll, a webhook is how those answers reach a CRM, a data warehouse or an automation platform without waiting for an export.
This guide covers how Zigpoll webhooks are created and signed, what the payload contains, how retries work, and the practices that keep a Zigpoll receiver reliable.
What are Zigpoll webhooks?
Each Zigpoll webhook subscribes to one survey and fires according to a delivery rule: on every submission, or only when the respondent completes the survey. Zigpoll sends a JSON POST to your HTTPS URL. Survey logic can also fire a webhook through the Trigger webhook action, for example only when someone picks a particular answer, and those deliveries use the same payload, signature and retry behaviour.
Every delivery is signed. The X-Zigpoll-Signature header looks like t=1754400000000,v1=5257a8..., where t is the send time in milliseconds and v1 is the hex HMAC-SHA256 of <t>.<raw body>, keyed with your account's Signing Secret.
Zigpoll webhook features
| Feature | Details |
|---|---|
| Configuration | Integrations > Webhooks, or the API (GET/POST/DELETE /webhooks, Premium plan and above) |
| Subscription model | One survey per webhook, up to 20 webhooks per survey |
| Delivery rules | Partial or complete submissions (submitted-responses), or completions only (completed-survey) |
| Signature header | X-Zigpoll-Signature: t=<unix ms>,v1=<hex> |
| Scheme | HMAC-SHA256, hex digest |
| Signed content | <t>.<raw body> |
| Secret | One Signing Secret per account, shown once. Rotation through Zigpoll support |
| Idempotency key | X-Zigpoll-Delivery-Id, the same on every attempt |
| Response window | 2xx within 10 seconds |
| Retries | 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours |
| Failure alerts | Email after a day with no successful delivery and at least one delivery given up on |
| Source IPs | Not published |
Common events
Zigpoll has no event catalogue. The body's type field tells you which delivery rule fired:
type | Fires |
|---|---|
submitted-responses | On every submission, even if the respondent stops partway |
completed-survey | Only once the respondent reaches the end of the survey |
Zigpoll documents the payload like this:
const payload = {
type: 'submitted-responses', // or 'completed-survey'
accountId: '',
pollId: '',
participantId: '',
metadata: {},
responses: {},
submissionsComplete: false, // did the respondent finish the survey?
timestamp: 1754400000000,
email: 'jane@example.com', // first email found for this respondent, or null
emails: ['jane@example.com'] // every email found, or []
};
That is a commented JavaScript object with empty IDs rather than a captured delivery, so log a real one before you build parsing around responses and metadata. timestamp is in milliseconds. Responses are batched briefly before sending, so one delivery can carry several answers from the same respondent. Surveys in anonymous mode always send email: null and emails: []. Zigpoll does not say which type a survey-logic webhook carries.
Setting up Zigpoll webhooks
- In the Zigpoll dashboard, go to Integrations and scroll to Webhooks.
- Create a webhook with a public
https://URL, the survey, and the delivery rule. - Copy the Signing Secret and store it server-side, for example as
ZIGPOLL_WEBHOOK_SECRET. Zigpoll shows it once.
The Signing Secret belongs to the account, not the webhook. Every webhook and every survey-logic webhook on the account uses it, and adding or deleting webhooks does not change it. There is no self-serve rotation: you ask Zigpoll support, and queued retries switch to the new secret at once.
Saved endpoints also receive a legacy X-Zigpoll-Secret header containing the webhook's own Secret Key. Zigpoll keeps it for older integrations and says to verify the signature instead. When you create a Hookdeck source for Zigpoll, paste the account's Signing Secret, not the per-webhook Secret Key.
Securing Zigpoll webhooks
Parse t and v1 from X-Zigpoll-Signature, compute an HMAC-SHA256 of <t>.<raw body> with the Signing Secret, and compare the hex digests in constant time.
const crypto = require('crypto');
const express = require('express');
function verifyZigpollSignature(rawBody, header, secret) {
if (!header || !secret) return false;
let t;
const candidates = [];
for (const part of header.split(',')) {
const [key, ...rest] = part.trim().split('=');
if (key === 't') t = rest.join('=');
else if (key === 'v1') candidates.push(rest.join('='));
}
if (!/^\d+$/.test(t || '') || candidates.length === 0) return false;
const expected = Buffer.from(
crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex'),
'utf8'
);
return candidates.some((candidate) => {
const received = Buffer.from(candidate.toLowerCase(), 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
app.post('/webhooks/zigpoll', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.ZIGPOLL_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifyZigpollSignature(req.body, req.get('x-zigpoll-signature'), secret)) {
return res.sendStatus(401);
}
const delivery_id = req.get('x-zigpoll-delivery-id');
const payload = JSON.parse(req.body.toString('utf8'));
// Deduplicate on delivery_id, branch on payload.type, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
def verify_zigpoll_signature(raw_body: bytes, header, secret: str) -> bool:
if not header or not secret:
return False
t, candidates = None, []
for part in header.split(","):
key, _, value = part.strip().partition("=")
if key == "t":
t = value
elif key == "v1":
candidates.append(value)
if not t or not (t.isascii() and t.isdigit()) or not candidates:
return False
expected = hmac.new(
secret.encode("utf-8"), t.encode("utf-8") + b"." + raw_body, hashlib.sha256
).hexdigest().encode("utf-8")
return any(hmac.compare_digest(c.lower().encode("utf-8"), expected) for c in candidates)
Zigpoll's own sample rejects any t more than 5 minutes from the current time. The Zigpoll webhook skill keeps that window off by default, so it accepts Hookdeck retries and replays of older events. If you turn the window on with ZIGPOLL_, note that Zigpoll re-signs each of its own retries with a fresh t, but Hookdeck retries and replays resend the original timestamp, so any that arrive after the window has passed fail the check. If you write your own check, remember that t is in milliseconds. Use X-Zigpoll-Delivery-Id to catch duplicates.
Make Zigpoll webhooks production-ready. Hookdeck Event Gateway verifies the
X-Zigpoll-Signatureheader, stores every survey response, and delivers it to your services with retries and replay you control.
Zigpoll webhook limitations and pain points
A dropped delivery is gone for good
The Problem: Answers submitted during an outage never reach your endpoint.
Why It Happens: Zigpoll retries a timeout, refused connection, 5xx, 429 or 408 five more times over about eight and a half hours, then stops. Any other 4xx is treated as permanent and not retried at all, and a DNS failure stops after three attempts. Once Zigpoll gives up, it does not send that response again. The answers stay in the dashboard, exports and API.
Workarounds:
- Return
2xxwithin 10 seconds and process afterwards. - Don't return
4xxfor transient problems such as a missing dependency. Use503so Zigpoll retries. - Backfill from Zigpoll's exports or API after an outage.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, so an outage on your side does not use up Zigpoll's retry ladder. You can then retry or replay events once your service is back.
Timestamps in milliseconds
The Problem: A freshness check rejects every delivery, or accepts deliveries from decades in the future.
Why It Happens: t in the signature header and timestamp in the body are milliseconds since the epoch, while most webhook schemes use seconds.
Workarounds:
- Divide
tby 1000 before comparing it with a time in seconds. - Remember that the signed string uses
texactly as sent, in milliseconds.
How Hookdeck Can Help: Hookdeck's Zigpoll source type verifies the HMAC over <t>.<body> with t as sent and applies no timestamp window, so the units never cause a failed verification.
One secret for the whole account
The Problem: Rotating the secret affects every survey at once, and you can't do it yourself.
Why It Happens: The Signing Secret is account-wide and only Zigpoll support can rotate it. Because every attempt is signed with the current secret, retries that are already queued switch to the new one immediately.
Workarounds:
- Keep the secret in one place that every receiver reads from.
- Coordinate the rotation with support so you can deploy the new value straight away.
How Hookdeck Can Help: With Hookdeck in front, the secret lives on one source. Update it there and every service behind it keeps receiving verified events.
Best practices
Verify against the raw body
Zigpoll warns that parsing and re-serialising the JSON can reorder keys and break the signature. Use express.raw() in Express, await request.text() in a Next.js route handler and await request.body() in FastAPI, and parse only after the check passes.
Deduplicate on X-Zigpoll-Delivery-Id
Delivery is at-least-once, and a slow response can produce a second delivery that you have already processed. The delivery ID stays the same across attempts, while the signature changes. See our guide to webhook idempotency.
Pick the delivery rule deliberately
submitted-responses fires on partial submissions too, so the same respondent can produce several deliveries as they progress. Check submissionsComplete, or use completed-survey if you only want finished surveys.
Acknowledge fast and process asynchronously
Zigpoll allows 10 seconds. Verify, store the payload, return 200, and do the CRM or warehouse write afterwards. See why to process webhooks asynchronously.
Conclusion
Zigpoll signs every delivery with X-Zigpoll-Signature: a millisecond t and a hex HMAC-SHA256 of <t>.<raw body>, keyed with the account-wide Signing Secret. Verify against the raw bytes, deduplicate on X-Zigpoll-Delivery-Id, keep any timestamp check optional, and answer within 10 seconds, because a delivery Zigpoll gives up on is never sent again.
Hookdeck Event Gateway sits in front of your receiver, checks every Zigpoll signature, and holds each response until your service has processed it.
Get started with Hookdeck for free and handle Zigpoll webhooks reliably in minutes.