Gareth Wilson Gareth Wilson

Guide to Zigpoll Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationIntegrations > Webhooks, or the API (GET/POST/DELETE /webhooks, Premium plan and above)
Subscription modelOne survey per webhook, up to 20 webhooks per survey
Delivery rulesPartial or complete submissions (submitted-responses), or completions only (completed-survey)
Signature headerX-Zigpoll-Signature: t=<unix ms>,v1=<hex>
SchemeHMAC-SHA256, hex digest
Signed content<t>.<raw body>
SecretOne Signing Secret per account, shown once. Rotation through Zigpoll support
Idempotency keyX-Zigpoll-Delivery-Id, the same on every attempt
Response window2xx within 10 seconds
Retries1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours
Failure alertsEmail after a day with no successful delivery and at least one delivery given up on
Source IPsNot published

Common events

Zigpoll has no event catalogue. The body's type field tells you which delivery rule fired:

typeFires
submitted-responsesOn every submission, even if the respondent stops partway
completed-surveyOnly 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

  1. In the Zigpoll dashboard, go to Integrations and scroll to Webhooks.
  2. Create a webhook with a public https:// URL, the survey, and the delivery rule.
  3. 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_TIMESTAMP_TOLERANCE_SECONDS, 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-Signature header, 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 2xx within 10 seconds and process afterwards.
  • Don't return 4xx for transient problems such as a missing dependency. Use 503 so 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 t by 1000 before comparing it with a time in seconds.
  • Remember that the signed string uses t exactly 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.