Webhook signing
Every webhook delivery is signed with HMAC-SHA256 using your endpoint’s signing secret. Verify the signature before parsing the payload into business logic, and keep the raw request body bytes. Verification depends on the exact payload sent, so re-serializing parsed JSON will produce a different digest.Signature headers
X-Cope-Signature is a structured value, not a bare digest. Parse out the t and v1 components before comparing. Comparing the raw header value against a computed digest always fails.
Verification steps
- Read the raw request body bytes before any JSON parsing.
- Parse
t(timestamp) andv1(hex signature) fromX-Cope-Signature. - Reject the delivery if
tis outside your tolerance window. COPE recommends 5 minutes to guard against replay. - Compute
HMAC_SHA256(signing_secret, "{t}.{raw_body}")and hex-encode it. - Compare against
v1with a constant-time comparison.
Worked example
For a delivery with:{"id":"evt_123",...}, the expected signature is:
v1 component, 5f6e...9c1d. Copy-paste verification snippets for Node, Python, PHP, Ruby, and Go are shown in the dashboard when an endpoint is created.
Common verification mistakes
- Comparing the raw
X-Cope-Signaturevalue against the digest. Extractv1=first. - Signing only the body. The signed string is
"{timestamp}.{body}", joined with a literal.. - Re-serializing the body. Parse-then-stringify changes key order, whitespace, or Unicode escaping; always HMAC the raw bytes as received.
- Using the wrong encoding. The signature is lowercase hex, not base64, and carries no
sha256=prefix. - Using the wrong secret. Secrets are per endpoint and shown only when the endpoint is created; a recreated endpoint gets a new secret and the old one stops verifying.
Consumer checklist
- Read the raw request body before JSON parsing.
- Verify the signature header with the endpoint secret configured for the subscription.
- Reject missing, malformed, or stale signatures.
- Deduplicate only after verification.
- Return 2xx only after durable acceptance.