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:
- Notifications are sent as soon as a new submission comes in.
- Each request carries two credentials:
Token, the workspace Verification Token in plain text, andHmac, a signature using the workspace Signing Secret. Survicate recommends the signature. - Every body includes
messageIdanddeliveryTries, 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:
{
"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
- In the Survicate panel, open Webhooks Settings (under the workspace's integrations settings).
- Copy the Signing Secret and store it as
SURVICATE_SIGNING_SECRET. The Verification Token on the same page is the plain-text alternative. - Add your endpoint, for example
https://example.com/webhooks/survicate, and choosequestionAnswered,surveyCompletedor both.
To route through Hookdeck, create a Survicate source with the Signing Secret and use its URL as the endpoint. For local development:
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 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
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.
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 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.eventand treat that as the event name. - Ignore
typeinside 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
200quickly once the response is stored. - Use
deliveryTriesto 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
surveyCompletedif you only need full responses. - Group per-answer events by
responseUuidin 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
questionAnsweredas 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.
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 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 verifies Survicate requests, routes the two events and retries delivery to your systems.
Get started with Hookdeck for free and handle Survicate webhooks reliably in minutes.