Skip to content

How HMAC webhook signing works

This page explains the HMAC-SHA256 signing scheme MapleGather uses on webhook deliveries — what it is, why it authenticates the sender, and how to verify a signature in your receiver.

MapleGather signs every webhook delivery using HMAC-SHA256 with a secret known only to you and MapleGather. When your receiver verifies the signature, it proves the delivery came from MapleGather and has not been tampered with in transit. The signing secret is shown once when you create the endpoint — store it now. If you lose it, use Rotate secret (webhook_secrets_rotate) to issue a new whsec_… secret in place without recreating the endpoint.

The signing secret is generated server-side when you create a webhook endpoint and returned in the create response exactly once. It is the HMAC key — you store it in your receiver’s environment as a secret; MapleGather never exposes it again in any API read response.

The signed headers are included on every POST delivery:

Header Contains
X-Webhook-Signature HMAC-SHA256 hex digest of the signed string
X-Webhook-Timestamp Unix timestamp (seconds) of when the delivery was dispatched

The signed string is constructed as:

<X-Webhook-Timestamp>.<raw request body>

That is: the timestamp value, a literal period (.), then the raw bytes of the request body (before any parsing).

Verification algorithm in your receiver:

  1. Read X-Webhook-Timestamp and X-Webhook-Signature from the request headers.
  2. Construct the signed string: <X-Webhook-Timestamp>.<raw request body>.
  3. Compute HMAC-SHA256 of the signed string using your stored signing secret (the key is raw bytes, not base64-decoded unless you stored it base64).
  4. Hex-encode the digest and compare it to X-Webhook-Signature. Use a constant-time comparison function (for example, hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js) to prevent timing attacks.
  5. If the comparison fails, return 400 or 403 and do not process the payload.
  6. Check that X-Webhook-Timestamp is within the last five minutes of the current UTC time. If it is older, reject the delivery. This is the replay-defense window.

Pseudo-code example:

import hmac, hashlib, time
def verify(secret: bytes, timestamp: str, body: bytes, signature: str) -> bool:
signed_string = f"{timestamp}.".encode() + body
expected = hmac.new(secret, signed_string, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return False
if abs(time.time() - int(timestamp)) > 300: # 5 minutes
return False
return True

HMAC authenticates the origin. Without signature verification, any server that knows your endpoint URL could post forged events — triggering false member creations, payment acknowledgements, or registrations in your system. The secret-bound HMAC means only MapleGather (which holds the same secret server-side) can produce a valid signature.

The five-minute timestamp window prevents replay attacks: a valid delivery captured in transit cannot be re-submitted hours later because the timestamp check will fail.

  • Replayed deliveries (via the delivery-log Replay action) regenerate the signature at the current timestamp. Your receiver should accept these — the replay timestamp is fresh, not the original.
  • If you lose the signing secret, use Rotate secret (webhook_secrets_rotate) to issue a new whsec_… secret in place — the endpoint id, URL, and event subscriptions are all preserved. You do not need to delete and recreate the endpoint.
  • The signing secret is distinct per endpoint. Multiple endpoints have independent secrets.