Webhook Signing Secrets
Every static webhook endpoint has its own signing secret. We use it to compute an HMAC-SHA256 over each event body. Your receiver verifies that HMAC to prove the event came from TapTree and was not tampered with in transit.
The plaintext signing secret is shown exactly once — directly after you create the endpoint or rotate its secret. We do not store the plaintext; if you lose it, you must rotate to generate a new one.
Headers on every delivery
Every delivery to a static endpoint carries these headers:
- Name
signature-algo- Type
- header
- Description
The signing algorithm. For static-endpoint v2 deliveries this is always
hmac-sha256-v2. (Legacy per-payment webhooks may emitsha256here.)
- Name
signature-method- Type
- header
- Description
Always
HMACfor both v1 and v2 deliveries.
- Name
signature-timestamp- Type
- header
- Description
Unix-timestamp (seconds) of when we signed the event. Used to reject stale replays.
- Name
signature-secret-id- Type
- header
- Description
Public identifier of the secret version that signed this event — e.g.
whsec_id_a3xq72k1for static endpoints, orwh_BDgCjn9hJHRfor legacy per-payment webhooks. During the rotation grace period, two valid IDs may circulate. You verify against the version matching this id.
- Name
signature- Type
- header
- Description
Hex-encoded HMAC-SHA256 of
${signature-timestamp}.${request_body_bytes}using the plaintext signing secret for the indicatedsignature-secret-id.
Verification steps
For every webhook request:
- Reject stale events: compute
now() - signature-timestamp. Reject if more than 5 minutes (300 seconds) in the past or 60 seconds in the future. - Look up the secret by id: match
signature-secret-idto the active secret(s) on your side. If you don't recognise the id, reject the request. - Compute the expected signature:
hmac_sha256(secret, "${timestamp}.${raw_body_bytes}"). Use the raw HTTP body bytes — not a parsed-and-re-serialized JSON object, because key ordering and whitespace would differ. - Constant-time compare: use
hash_equals(PHP),crypto.timingSafeEqual(Node),hmac.compare_digest(Python). Never use===/==for HMAC comparison — it leaks timing information.
If the signature matches, you can trust the event body. If not, return 4xx so we mark the delivery as failed.
See Webhook Testing for working snippets in Node, PHP, Python and Ruby.
Rotation
Rotate a secret to invalidate the old one — for example, after a suspected compromise, on a regular schedule, or before offboarding an engineer who had access.
Rotation flow:
- Open the endpoint's row menu in the Dashboard and choose Rotate secret.
- We generate a fresh secret and a new
signature-secret-id. Both are shown once. - Copy the new secret into your receiver's secret store. Configure it to accept either the old or new secret during the grace period.
- After 24 hours, the old secret expires. Subsequent events carry only the new
signature-secret-id. Remove the old secret from your store.
During the 24-hour grace period, both signature-secret-id values can appear in headers. Your receiver must look up the secret by id and verify against the matching version. Hard-coding "the current secret" without an id lookup breaks the rotation contract.
Why the plaintext is only shown once
Signing secrets are encrypted at rest. The plaintext is shown to you exactly once — in the Dashboard, at create or rotation time. Store it in your secret manager immediately. A database-level read does not yield usable signing material; if you lose the secret you must rotate to obtain a new one.
What to do if you lose the secret
- Open the endpoint in the Dashboard.
- Trigger Rotate secret (see above).
- Update your receiver with the new secret immediately.
- The previous secret expires after the 24-hour grace period. If you need a hard cutover, contact support and we can shorten the grace window.
There is no way to recover an old plaintext — by design. The reveal action in the Dashboard returns the current active secret only and is audit-logged for compliance.
Audit trail
Every secret operation — secret_revealed, rotated — is recorded in the endpoint's audit log. Reveal and rotation entries include the actor user id, IP address, user agent and timestamp. This is your forensic record if a secret leaks.