Agent skill
SurveyMethods Webhooks Skill
Receive and verify SurveyMethods webhooks. Use when setting up SurveyMethods webhook handlers, debugging X-SurveyMethods-Signature verification, or handling survey events like response.completed, response.created, survey.created and collector.created.
Install this skill
npx skills add hookdeck/webhook-skills --skill surveymethods-webhooks
When to Use This Skill
- How do I receive SurveyMethods webhooks?
- How do I verify the
X-SurveyMethods-Signatureheader on a SurveyMethods webhook? - How do I handle
response.completedorsurvey.createdevents? - Why is my SurveyMethods webhook signature verification failing?
- How do I deduplicate SurveyMethods deliveries (
eventId)?
Verification (core)
SurveyMethods sends X-SurveyMethods-Signature: sha256=<hex>, where <hex> is the lowercase hex HMAC-SHA256 of the raw request body, keyed with the webhook's signing secret (its UTF-8 bytes, used as-is). Only the body is signed: there is no timestamp, so there is no replay window to check. Strip a leading sha256= (accept the bare digest too), validate the 64-char hex shape, then compare in constant time. Verify the raw bytes before parsing.
Node (check the hex shape first: Buffer.from(x, 'hex') silently drops bad characters):
const crypto = require('crypto');
function verifySurveyMethodsSignature(rawBody, signatureHeader, secret) {
if (typeof signatureHeader !== 'string' || !secret) return false;
let received = signatureHeader.trim();
if (received.startsWith('sha256=')) received = received.slice('sha256='.length);
if (!/^[0-9a-f]{64}$/i.test(received)) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
return crypto.timingSafeEqual(Buffer.from(received, 'hex'), expected);
}
Python (compare bytes so a non-ASCII header gives 401, not a 500):
import hashlib, hmac, re
def verify_surveymethods_signature(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
if not signature_header or not secret:
return False
received = signature_header.strip()
if received.startswith("sha256="):
received = received[len("sha256="):]
if not re.fullmatch(r"[0-9a-fA-F]{64}", received):
return False
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(received.lower().encode("utf-8"), expected.encode("utf-8"))
A missing or empty header, a malformed value or a mismatch is a 401. Express handlers must read the body with express.raw({ type: '*/*' }) so any content type verifies the exact bytes.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Events
Dispatch on the body's top-level eventType (also sent as the X-SurveyMethods-Event-Type header). eventId (also X-SurveyMethods-Event-Id) identifies the event.
| Event | Triggered When |
|---|---|
response.completed | A respondent completes a survey response |
response.created | A survey response is started |
survey.created | A survey is created |
collector.created | A collector is created |
Delivery is POST; any 2xx within 30 seconds acknowledges. Otherwise it is retried (up to 5 attempts in all, with growing spacing), so delivery is at-least-once: deduplicate on eventId.
Environment Variables
SURVEYMETHODS_WEBHOOK_SECRET=your_signing_secret # shown once when you save the webhook (Account > Webhooks)
Setup with Hookdeck
Create a Hookdeck source of type SurveyMethods and paste the webhook's signing secret into its webhook verification settings. Point the SurveyMethods webhook at the Hookdeck source URL.
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 surveymethods --path /webhooks/surveymethods
Reference Materials
- references/overview.md - SurveyMethods webhook concepts, events and payload
- references/setup.md - Create a webhook and get the signing secret
- references/verification.md - Signature verification details and gotchas