> For the complete documentation index, see [llms.txt](https://docs.zigpoll.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zigpoll.com/integrations/webhooks.md).

# Webhooks

### 🔑 How to Create A Zigpoll Webhook

* Log in to your Zigpoll dashboard.
* Go to Integrations → Scroll down to Webhooks.
* Create a new webhook by filling in the form.

Endpoints must be a public `https://` URL. You can add up to **20 webhooks per survey**.

Webhooks can also be managed programmatically via the [API](/web-api.md) (`GET`/`POST`/`DELETE /webhooks`). API access requires an **Advanced** plan or higher, and the webhook endpoints require an **admin-tier** API key — keys belonging to Editor or Read only team members are rejected. Note that the signing secret is returned **only once**, in the response to the create call.

### 📬 When we send

Each webhook subscribes to one survey, and you choose when it fires:

| Delivery rule                                      | Fires                                                                                               |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Send if the survey is partially or fully completed | On every submission, even if the respondent stops partway. Payload `type` is `submitted-responses`. |
| Send only if the survey is fully completed         | Only once the respondent reaches the end of the survey. Payload `type` is `completed-survey`.       |

Responses are briefly batched before we send, so a respondent who answers several questions in a row produces one delivery containing all of their answers rather than one per question.

### 📦 Payload

We POST valid JSON to your endpoint:

```javascript
{
  type: 'submitted-responses',   // or 'completed-survey'
  accountId: '',
  pollId: '',
  participantId: '',
  metadata: {},
  responses: {},
  submissionsComplete: false,    // did the respondent finish the survey?
  timestamp: 1754400000000
}
```

### 🧾 Headers

| Header                   | Description                                                                                                                                                                |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`           | Always `application/json`.                                                                                                                                                 |
| `X-Zigpoll-Signature`    | HMAC signature of the request body — see below.                                                                                                                            |
| `X-Zigpoll-Delivery-Id`  | Unique id for this event. **Stays the same across retries** — use it to ignore duplicates.                                                                                 |
| `X-Zigpoll-Attempt`      | Which attempt this is, starting at `1`.                                                                                                                                    |
| `X-Zigpoll-Triggered-At` | When this attempt was sent.                                                                                                                                                |
| `X-Zigpoll-Secret`       | *(Legacy)* Your webhook's Secret Key, shown next to the endpoint in the dashboard. Kept for consumers built before signing existed — verify `X-Zigpoll-Signature` instead. |

### 🔐 Verifying the signature

Every delivery is signed with your account's **Signing Secret** (Integrations → Webhooks). Verifying it proves the request came from Zigpoll and hasn't been altered.

The header looks like `t=1754400000000,v1=5257a8...`, where `t` is the send time in milliseconds and `v1` is `HMAC-SHA256` of `"{t}.{raw request body}"`.

Verify against the **raw request body** — parsing and re-serializing the JSON can reorder keys and break the comparison.

```javascript
const crypto = require('crypto');

function verifyZigpollSignature(rawBody, header, signingSecret) {
  const parts = Object.fromEntries(
    String(header || '').split(',').map((kv) => kv.split('='))
  );
  if (!parts.t || !parts.v1) return false;

  // Reject anything older than 5 minutes to prevent replays.
  if (Math.abs(Date.now() - Number(parts.t)) > 5 * 60 * 1000) return false;

  const expected = crypto
    .createHmac('sha256', signingSecret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

### 🔁 Retries

Respond with a `2xx` status within **10 seconds**. If your endpoint times out, refuses the connection, or returns a `5xx`, `429`, or `408`, we retry up to five more times:

**1 minute → 5 minutes → 30 minutes → 2 hours → 6 hours**

Any other `4xx` response is treated as a permanent rejection and is not retried. Deleting a webhook cancels its pending retries.

If your endpoint's hostname doesn't resolve at all (a DNS failure rather than a refused connection), we stop after **three** attempts spread over the first few minutes instead of running the full ladder.

Delivery is at-least-once: if your endpoint processes a request but answers too slowly, you may receive it again. Deduplicate on `X-Zigpoll-Delivery-Id`, which is identical across every attempt of the same event.

Once the last retry fails, we stop. **That response is not sent again** — the answers stay in your Zigpoll dashboard and remain available through exports and the API, but they will not arrive at your endpoint.

### 🚨 If your endpoint stops working

We'll email you when a webhook has stopped delivering entirely, so a broken endpoint doesn't go unnoticed while responses pile up.

**When it's sent.** An endpoint qualifies once it has gone a full day with no successful delivery *and* at least one delivery given up on — whether that was after the full retry ladder or immediately, as with a permanent `4xx`. The day-long wait means a brief outage that recovers on its own never triggers an email.

**How often.** Once when the problem is confirmed, then a reminder each week, up to three emails in total. A single successful delivery resets this — if the endpoint breaks again later, you'll be alerted again.

**Who receives it.** Everyone in **Notification Recipients** on your account's [Notifications](/notifications.md) settings, or the account owner if you haven't set that list.

Each email identifies the specific webhook, so you can act on it without opening the dashboard:

| Field           | What it tells you                                                                            |
| --------------- | -------------------------------------------------------------------------------------------- |
| Endpoint        | The full URL, exactly as configured.                                                         |
| Survey          | Which survey feeds this webhook — plus the question, for webhooks fired from question logic. |
| Trigger         | The delivery rule or logic condition that fires it.                                          |
| Configured in   | Your Webhooks settings, or the survey's Behavior tab for logic-triggered webhooks.           |
| What went wrong | The status code or connection error from the last attempt.                                   |
| Responses lost  | How many deliveries were given up on in the past week.                                       |

**To stop the emails**, fix the endpoint or remove the webhook. Removing it also cancels any retries still queued.

Some common causes:

* **`404`** — the route no longer exists, or you're using a testing URL. n8n's `/webhook-test/` URLs only accept data while the workflow editor is open; activate the workflow and switch to its `/webhook/` production URL.
* **`401` or `403`** — your endpoint is rejecting our request. Zigpoll authenticates with the `X-Zigpoll-Signature` header, not a bearer token or API key.
* **`410`** — a hosted hook (Make, Zapier, and similar) has been deleted or expired. Create a new one and update the endpoint.
* **Timeouts** — your endpoint took longer than 10 seconds. Acknowledge the request with a `2xx` first, then do the slow work in the background.

### ⚡ Webhooks from survey logic

You can also trigger a webhook conditionally with the **Trigger webhook** action in question logic or survey action logic — for example, only when someone selects a particular answer or completes the survey.

These deliveries use the same payload, signature, retry behavior, and delivery id as the webhooks above. They do not include the `X-Zigpoll-Secret` header, because the URL is configured on the rule rather than as a saved endpoint — verify `X-Zigpoll-Signature` instead.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.zigpoll.com/integrations/webhooks.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
