Skip to main content

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

  1. Read the raw request body bytes before any JSON parsing.
  2. Parse t (timestamp) and v1 (hex signature) from X-Cope-Signature.
  3. Reject the delivery if t is outside your tolerance window. COPE recommends 5 minutes to guard against replay.
  4. Compute HMAC_SHA256(signing_secret, "{t}.{raw_body}") and hex-encode it.
  5. Compare against v1 with a constant-time comparison.

Worked example

For a delivery with:
and raw body {"id":"evt_123",...}, the expected signature is:
That value must equal the 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-Signature value against the digest. Extract v1= 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 after a rotation only the new secret verifies: the old one stops at once.
4xx responses from your endpoint are treated as terminal. The delivery is recorded as failed and is not retried automatically. Fix verification, then replay the delivery from the webhook details view in the COPE dashboard.

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.

Rotating the signing secret

Rotate an endpoint’s signing secret with POST /v1/webhooks/endpoints/{id}/secret-rotations, sent from your server with an Idempotency-Key header and a secret API key that can make changes (a read-only key is refused). The 201 response carries the new secret as signing_secret. The COPE dashboard does not rotate secrets.
  • The new secret replaces the old one immediately. There is no overlap: every delivery sent after the rotation — new events, automatic retries and replays alike — is signed with the new secret, and the old secret no longer verifies.
  • Store the secret from the first response. Retrying the request with the same Idempotency-Key returns the 201 again but without signing_secret, because COPE keeps no copy to return. The rotation itself did happen, so if you lost the response, rotate again with a new Idempotency-Key.
  • Recover what the switch rejected. Deploy the new secret to your receiver as soon as the response arrives. A delivery your endpoint rejects with a 4xx in the meantime is recorded as failed and not retried automatically. Once your receiver verifies with the new secret, replay those deliveries from the webhook details view in the dashboard, one at a time with POST /v1/webhooks/deliveries/{delivery_id}/replay, or for a time window with POST /v1/webhooks/replay-jobs. A replay is signed with the current secret.