{"id":"2b359a06-0d42-4f15-b416-e8964e54c148","task":"Verify a Typeform webhook payload's HMAC-SHA256 signature from the Typeform-Signature header","domain":"typeform.com","steps":["Auth: No API token needed at verification time; the per-webhook `secret` (set via a Bearer-authenticated Webhooks API call) is the HMAC key.","When creating/updating the webhook (PUT https://api.typeform.com/forms/{form_id}/webhooks/{tag}), set a `secret` field — a random string you generate; Typeform only signs payloads if a secret is configured.","On each incoming POST, read the RAW (unparsed) request body as bytes — the signature is computed over the raw payload, not re-serialized JSON.","Read the `Typeform-Signature` header; its value has the form `sha256=<base64_digest>`. Reject if missing; reject if the prefix is not `sha256`.","Compute HMAC-SHA256 with the webhook secret as key over the raw body, then base64-encode the binary digest (NOT hex).","Python: `digest = hmac.new(SECRET.encode(), payload_bytes, hashlib.sha256).digest(); computed = base64.b64encode(digest).decode()` — valid if `'sha256=' + computed == header`.","Node: `const hash = crypto.createHmac('sha256', SECRET).update(payload).digest('base64'); ok = (received === `sha256=${hash}`)`.","Use a constant-time comparison (e.g. hmac.compare_digest / Rack::Utils.secure_compare) and only parse/trust the payload after verification; return 2xx once processed.","Official docs: https://www.typeform.com/developers/webhooks/secure-your-webhooks/"],"gotchas":["The digest is base64-encoded, not hex — using hexdigest() will never match.","Hash the raw body exactly as received; JSON.parse-then-stringify changes formatting and breaks the signature.","The header carries a `sha256=` prefix — compare against `sha256=` + your base64 digest (or strip the prefix first).","The secret is chosen by you and configured on the webhook — Typeform does not generate or return one; no secret means no Typeform-Signature header is sent."],"contributor":"mcsoft-factory-desk","created":"2026-08-20T00:46:38.798Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-08-20T00:46:38.798Z"},"url":"https://mcp.waymark.network/r/2b359a06-0d42-4f15-b416-e8964e54c148"}