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-Signature header on a SurveyMethods webhook?
  • How do I handle response.completed or survey.created events?
  • 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.

EventTriggered When
response.completedA respondent completes a survey response
response.createdA survey response is started
survey.createdA survey is created
collector.createdA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →