Skip to main content
AhaSend signs every webhook and inbound route request with HMAC-SHA256; this page shows how to verify the signature before trusting the payload. Signed webhooks and routes are available on Free, Pro and Max. See plans and features.

Which Headers Does AhaSend Send?

All webhook and route requests include Standard Webhooks security headers for verification:
  • webhook-id: Unique identifier for the webhook event (used as idempotency key)
  • webhook-timestamp: Unix timestamp when the webhook was sent
  • webhook-signature: HMAC signature of the payload using your webhook secret
Signatures are HMAC-SHA256 over id.timestamp.body, following the Standard Webhooks specification.
The HMAC key is the literal UTF-8 bytes of your webhook secret, exactly as returned when the webhook was created — including any prefix. Do not Base64-decode the secret and do not strip a prefix before signing.Some Standard Webhooks libraries Base64-decode the secret in their default constructor. Those constructors derive a different key and reject valid AhaSend deliveries. Use your library’s raw-key mode instead — for example NewWebhookRaw in Go, or new Webhook(secret, { format: "raw" }) in JavaScript.
The easiest path is a verifier that already implements this correctly, such as the official AhaSend SDKs. Whatever you use, verify against the raw request body bytes (before any JSON parsing), and reject deliveries whose webhook-timestamp is outside your tolerance window in either direction to prevent replay.

Which Secret Should I Use?

Webhook secrets start with aha-whsec-; route secrets start with aha-rsec-. Use the entire secret, including its prefix, as the HMAC-SHA256 key. Do not base64-decode it. The signed input is the delivery ID, a dot, the Unix timestamp, a dot, and the exact raw request body. The webhook-signature header contains v1,<base64 signature> values separated by spaces; accept a delivery if any supported signature matches. The recommended timestamp tolerance is five minutes in either direction, matching the AhaSend SDK default. The webhook-id header identifies a delivery. Payload webhook_id identifies the configured webhook, and route_id identifies a route. Deduplicate the header value only after verification, atomically with durable work. Return 2xx for verified unknown event types and already-processed deliveries.

Runnable Verification Examples

For a route delivery, set AHASEND_WEBHOOK_SECRET in these examples to that route’s aha-rsec- secret; the verification code is the same. For an email event, use that webhook’s aha-whsec- secret. Save an incoming raw body as payload.json without reformatting it. Set AHASEND_WEBHOOK_SECRET, WEBHOOK_ID, WEBHOOK_TIMESTAMP, and WEBHOOK_SIGNATURE to the secret and headers from that same delivery. Run the check while its timestamp is within five minutes. These scripts print only whether verification succeeded. Install the package used by your chosen example: npm install @ahasend/sdk@0.2.1 or npm install standardwebhooks for Node.js; go mod init example.com/verify followed by go get github.com/AhaSend/ahasend-go or go get github.com/standard-webhooks/standard-webhooks/libraries/go for Go; python3 -m pip install standardwebhooks for Python. PHP below uses its built-in HMAC functions.
Python’s bytes constructor preserves the raw key. The PHP example computes base64(HMAC-SHA256(secret_utf8, id + "." + timestamp + "." + body)) directly. It avoids the current Standard Webhooks PHP fromRaw constructor issue. See the upstream Python and PHP implementations. In an HTTP handler, enforce the 30,000,000-byte limit while reading the stream, before buffering. Keep a lower limit if your hosting platform requires one. SDK adapters already bound their reads; see the Node.js SDK and Go SDK examples.