# Guide to Survicate Webhooks: Features and Best Practices

Survicate runs customer feedback surveys across channels. Its webhooks push responses out as they arrive, one per answer or one per completed survey, so you can route detractor scores to support, enrich a CRM contact or store raw feedback in your warehouse.

This guide covers Survicate's two events and their unusual key-based envelope, why to verify the `Hmac` header rather than the plain `Token`, and the delivery details Survicate doesn't publish.

## What are Survicate webhooks?

Survicate webhooks are set up per workspace. The **Webhooks Settings** section of the Survicate panel holds the workspace's Verification Token and Signing Secret, which apply to every webhook Survicate sends from that workspace.

Survicate documents two events. `questionAnswered` fires for each answer to each question, so a five-question survey produces five requests. `surveyCompleted` fires once, and only when the respondent finishes the whole survey, with all the answers in one array.

## Survicate webhook features

| Feature | Details |
| --- | --- |
| Signature | hex HMAC-SHA256 |
| Signature header | `hmac` |
| Body | JSON |

From Survicate's [webhook documentation](https://developers.survicate.com/webhooks/):

* Notifications are sent as soon as a new submission comes in.
* Each request carries two credentials: `Token`, the workspace Verification Token in plain text, and `Hmac`, a signature using the workspace Signing Secret. Survicate recommends the signature.
* Every body includes `messageId` and `deliveryTries`, the number of attempts so far.

## Events and payloads

| Event | Meaning |
| --- | --- |
| `questionAnswered` | Question answered event is triggered upon each respondent's answer. Each answer to every single question will be sent along with respondent data. |
| `surveyCompleted` | Survey answer event sends webhooks upon each survey completion. Please note that this event will be triggered only when the whole survey is completed. |

The event name isn't a field. Every body is `{ messageId, deliveryTries, event: { <name>: { ... } } }`, and the one key inside `event` tells you which event it is. Inside `questionAnswered` there's also a `type` field, but that's the question type, such as a choice or text question, not the event.

The `questionAnswered` example from Survicate's [events page](https://developers.survicate.com/webhooks/events/):

```json
{
  "messageId": "c0101a21572ca643fd9f9ad40a5a4650",
  "deliveryTries": 0,
  "event": {
    "questionAnswered": {
      "responseUuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "survey": {
        "id": "b92b6ecae6dd7207",
        "name": "Satisfaction"
      },
      "url": "https://survey.survicate.com/43f2d5789346d6ad",
      "timestamp": "2019-05-10T06:46:02+00:00",
      "respondent": {
        "id": "534759",
        "uuid": "279cd4ab-8a07-4e9e-ac79-bcea32792bbc",
        "platform": "OSX",
        "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36",
        "attributes": [],
        "links": []
      },
      "question": {
        "id": 12123,
        "body": "How would you rate our customer service?"
      },
      "choice": {
        "id": 21573,
        "body": "Great",
        "comment": null
      }
    }
  }
}

```

`surveyCompleted` has the same `responseUuid`, `survey`, `url`, `timestamp` and `respondent` fields, plus a `questionsAnswered` array with one entry per answer. Answer fields depend on the question type: a choice question has `choice` with `body` and `comment`, while a text question has `text`. Survicate's question types page lists the rest. `respondent.attributes` holds any custom attributes you've passed to Survicate.

## Setting up Survicate webhooks

1. In the Survicate panel, open **Webhooks Settings** (under the workspace's integrations settings).
2. Copy the **Signing Secret** and store it as `SURVICATE_SIGNING_SECRET`. The **Verification Token** on the same page is the plain-text alternative.
3. Add your endpoint, for example `https://example.com/webhooks/survicate`, and choose `questionAnswered`, `surveyCompleted` or both.

To route through Hookdeck, create a Survicate source with the Signing Secret and use its URL as the endpoint. For local development:

```bash
npx hookdeck-cli listen 3000 survicate --path /webhooks/survicate

```

## Securing Survicate webhooks

The `Hmac` header holds the lowercase hex HMAC-SHA256 of the request body, keyed with the Signing Secret. Survicate's [security page](https://developers.survicate.com/webhooks/security/) notes that the hash must be in lowercase hex. The `Token` header is sent in plain text on every request, so anyone who sees one request has it. Survicate describes the HMAC as more secure, and it's what Hookdeck checks.

### Node.js verification

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

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['hmac'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

```

The handler finds the event by its key, checks the inner object, and stores the response by `messageId`.

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

const app = express();

app.post('/webhooks/survicate', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.SURVICATE_SIGNING_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  let body;
  try {
    body = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!body || typeof body !== 'object' || Array.isArray(body)) return res.sendStatus(400);
  if (!body.event || typeof body.event !== 'object') return res.sendStatus(400);

  const [eventName] = Object.keys(body.event);
  const detail = body.event[eventName];
  if (!detail || typeof detail !== 'object') return res.sendStatus(400);

  try {
    if (await seenMessage(body.messageId)) return res.sendStatus(200);
    await saveResponse(eventName, {
      responseUuid: detail.responseUuid,
      surveyId: detail.survey?.id,
      respondentId: detail.respondent?.id,
      answers: Array.isArray(detail.questionsAnswered) ? detail.questionsAnswered : [detail],
    });
  } catch {
    return res.sendStatus(503);
  }
  res.sendStatus(200);
});

```

Hookdeck's Survicate source checks the `Hmac` header and ignores `Token`. It responds `200` and records a mismatch as failed verification. With no timestamp in Survicate's signature, Hookdeck applies no age limit either.

> **Make Survicate webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) keeps every answer Survicate sends, so feedback isn't lost while your CRM or warehouse sync is down.

## Survicate webhook limitations and pain points

### The event type is a key name

**The Problem:** Generic routers that look for a `type` or `event` field can't route Survicate requests, and `questionAnswered.type` looks like an event type but isn't.

**Why It Happens:** Survicate nests the payload under a key named after the event, inside `event`.

**Workarounds:**

* Read the single key of `body.event` and treat that as the event name.
* Ignore `type` inside the payload for routing.

**How Hookdeck Can Help:** Hookdeck filters can match on the presence of `event.questionAnswered` or `event.surveyCompleted` and route each to its own destination.

### Delivery behaviour is undocumented

**The Problem:** You don't know when Survicate retries or for how long.

**Why It Happens:** Bodies include `deliveryTries`, which shows retries exist, but no schedule, timeout or success status is documented.

**Workarounds:**

* Return `200` quickly once the response is stored.
* Use `deliveryTries` to spot requests that needed several attempts.

**How Hookdeck Can Help:** Hookdeck accepts each request at once and retries your service on a schedule you set.

### Per-answer events multiply quickly

**The Problem:** A busy survey with many questions generates a request for every answer.

**Why It Happens:** `questionAnswered` fires for each answer to each question.

**Workarounds:**

* Use `surveyCompleted` if you only need full responses.
* Group per-answer events by `responseUuid` in your store.

**How Hookdeck Can Help:** Hookdeck absorbs bursts and can rate-limit delivery to your service.

### Partial responses don't produce surveyCompleted

**The Problem:** Respondents who stop halfway never appear in `surveyCompleted` data.

**Why It Happens:** Survicate sends `surveyCompleted` only when the whole survey is completed. Whether other partial-response events exist isn't documented.

**Workarounds:**

* Subscribe to `questionAnswered` as well if partial answers matter.
* Reconcile counts against Survicate's own reports.

**How Hookdeck Can Help:** Routing both events through Hookdeck lets you keep per-answer data in one place and full responses in another.

## Best practices

### Verify the Hmac, not the Token

The plain `Token` proves little once it's been logged anywhere. Use the HMAC, as Survicate recommends.

### Deduplicate on messageId

Survicate describes `messageId` as the webhook ID. Store it before writing the response, so a resend of the same message is skipped, as described in our [guide to idempotent webhook handling](/webhooks/guides/implement-webhook-idempotency).

### Handle each question type

Write a small adapter per question type you use (choice, text and so on) instead of assuming `choice` is always present.

### Write later, respond now

Queue the response and acknowledge it, then sync to your CRM. Our article on [processing webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) explains why.

## Conclusion

Survicate sends per-answer and per-completion webhooks with the event name as a key inside `event`, signed with a lowercase hex HMAC-SHA256 in `Hmac`. Verify the HMAC rather than the plain token, route on the key name, and deduplicate on `messageId`, since the retry policy isn't published.

[Hookdeck Event Gateway](https://hookdeck.com) verifies Survicate requests, routes the two events and retries delivery to your systems.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Survicate webhooks reliably in minutes.