Verify Webhook Signatures
Check HMAC-SHA256 signatures on event and decision requests.
Penny uses the same outbound signing scheme for asynchronous event deliveries and synchronous Live Decisioning requests. Configure a signing secret on each endpoint whose requests you want to verify. Signing is separate from the optional bearer token.
Configure signing
Webhook creation leaves signing unconfigured. After setup finishes, call POST /notifications/webhooks/{notification_endpoint_id}/signing_secret to create a secret (201). Creation fails with 422 if a secret already exists. Use POST /notifications/webhooks/{notification_endpoint_id}/signing_secret/rotate to replace an existing secret (200); rotation fails with 422 if no secret exists. When a signing_secret is present, each event or decision request carries two additional headers:
The signing secret itself is never sent in a request; only the timestamp and signature are. The signed payload is the timestamp, a literal period, and the raw request body, all as UTF-8 bytes exactly as received:
To verify a request:
- Read the
Penny-Webhook-TimestampandPenny-Webhook-Signatureheaders. - Reject the request if the timestamp is more than a few minutes (5 is a reasonable window) away from your current time. Because the timestamp is part of the signed payload, an attacker can’t change it without invalidating the signature — this is what stops a captured delivery from being replayed later.
- Recompute HMAC-SHA256 over
signed_payloadusing yoursigning_secret, hex-encode the digest, and prefix it withsha256=. - Compare the result to the
Penny-Webhook-Signatureheader using a constant-time comparison. Only process the request if they match.
Always hash the raw body bytes as they arrived — parse the JSON only after the signature checks out. Re-serializing the parsed object can change whitespace or key order and produce a different digest.
Signing-secret creation and rotation return only:
Store this value securely when it is returned. Ordinary webhook responses expose has_signing_secret and never include the secret, even with reveal_sensitive=true or when retrieving history. Use the entire penny_whsec_... string as the HMAC key. Rotation takes effect immediately for new requests; there is no overlap period. Requests already in flight may still carry the previous signature. If you lose the response, rotate again to obtain a new secret.
The endpoint must already be provisioned before you create its signing secret. Rotate the secret if it is lost or compromised, then update your receiver to use the new value. If you operate multiple endpoints, store the appropriate secret for each endpoint; do not choose one based only on unverified body data.