# 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):

```javascript
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):

```python
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/surveymethods-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/surveymethods-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/surveymethods-webhooks/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

```bash
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

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 surveymethods --path /webhooks/surveymethods

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/surveymethods-webhooks/references/overview.md) - SurveyMethods webhook concepts, events and payload
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/surveymethods-webhooks/references/setup.md) - Create a webhook and get the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/surveymethods-webhooks/references/verification.md) - Signature verification details and gotchas