> 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

Receive survey responses at your own endpoint, signed and retried

### 🔑 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 a **Premium** plan or higher, and the webhook endpoints require an **Owner, Admin, or Billing Admin** 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,
  email: 'jane@example.com',     // first email found for this respondent, or null
  emails: ['jane@example.com']   // every email found, or []
}
```

`email` and `emails` are gathered from the respondent's answers, their participant metadata and the response metadata. Surveys in [anonymous mode](/polls/display-rules.md#anonymous-mode) always send `email: null` and `emails: []`.

### 🧾 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.

### 🗝️ Signing secret questions

* **One secret per account.** The Signing Secret belongs to the account, not to a webhook, so every webhook on the account and every [survey logic webhook](#webhooks-from-survey-logic) is signed with the same secret. It is created when the account's first webhook is created, or on the first delivery for accounts whose webhooks predate signing, and it is shown once in **Integrations → Webhooks**.
* **Adding or deleting a webhook does not change it.** Creating a second endpoint reuses the existing secret, so a verifier you wrote for the first keeps working.
* **Retries are re-signed.** Each attempt is signed fresh, with the attempt's own `t` timestamp and the secret as it is at that moment, so a verifier that rejects old timestamps will still accept a retry that arrives hours later. Use `X-Zigpoll-Delivery-Id`, not the signature, to detect duplicates.
* **Rotation.** There is no self-serve rotate button. Contact <support@zigpoll.com> to rotate the secret; because every attempt uses the current secret, queued retries switch to the new one immediately.

### 🚨 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 **Settings → Workflows → Actions** 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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
