Skip to content

Verify a signed webhook

You’ll get: a receiver that only acts on a webhook that genuinely came from your InfraInbox instance, using the same check on both a Python and a Go/Node stack.

  • A webhook destination already set up in InfraInbox, pointed at your receiver’s URL (see Signed webhook).
  • The signing secret InfraInbox showed you when you created it, shaped like whsec_….

Every webhook InfraInbox sends follows the Standard Webhooks convention. Three headers ride along with the JSON body:

Header Holds
webhook-id The delivery’s ID — the same value on every retry of the same notice, so your receiver can drop duplicates.
webhook-timestamp Unix seconds, when this attempt was signed.
webhook-signature v1, followed by the base64-encoded HMAC-SHA256 of the signed content, keyed with your secret.

The signed content is exactly {webhook-id}.{webhook-timestamp}.{raw body} — the two header values, each followed by a literal ., then the request body bytes exactly as sent (compute the signature before any pretty-printing or re-encoding). The secret itself is whsec_ followed by the base64 encoding of the actual key; decode past the prefix before using it in the HMAC.

import hashlib
import hmac
import base64
def verify(secret: str, webhook_id: str, timestamp: str, body: bytes, signature_header: str) -> bool:
key = base64.b64decode(secret.removeprefix("whsec_"))
signed_content = f"{webhook_id}.{timestamp}.".encode() + body
expected = "v1," + base64.b64encode(
hmac.new(key, signed_content, hashlib.sha256).digest()
).decode()
# InfraInbox sends exactly one "v1,<sig>" value (never several
# space-separated ones — that's a generic Standard Webhooks allowance
# for other senders, not something this server's sender does).
return hmac.compare_digest(expected, signature_header)
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"strings"
)
// InfraInbox sends exactly one "v1,<sig>" value in webhook-signature — never
// several space-separated ones — so a plain equality check is enough.
func verify(secret, webhookID, timestamp string, body []byte, signatureHeader string) bool {
key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
if err != nil {
return false
}
mac := hmac.New(sha256.New, key)
mac.Write([]byte(webhookID + "." + timestamp + "."))
mac.Write(body)
expected := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signatureHeader))
}
const crypto = require('crypto');
// InfraInbox sends exactly one "v1,<sig>" value in webhook-signature — never
// several space-separated ones — so a plain equality check is enough.
function verify(secret, webhookId, timestamp, body, signatureHeader) {
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = 'v1,' + crypto
.createHmac('sha256', key)
.update(`${webhookId}.${timestamp}.`)
.update(body) // the raw request body Buffer, not a re-serialized object
.digest('base64');
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Whatever language you use, read the three headers (webhook-id, webhook-timestamp, webhook-signature) and the raw, unparsed request body — verify the signature first, and only then parse the body as JSON.

  1. In InfraInbox, open the webhook destination and click Send test.
  2. Your receiver should log a valid signature for the test payload; deliberately change one character of the secret and confirm it now reports invalid.
  3. Trigger a real incident and confirm the same check passes on a real payload — the body’s shape is documented in Signed webhook.
Symptom Likely cause
Every signature fails, including the test You’re hashing a re-serialized/pretty-printed body instead of the exact bytes InfraInbox sent — read the request body as raw bytes before any JSON parsing.
Works, then breaks after regenerating the secret The webhook destination’s secret was regenerated (there’s no overlap period — the old secret stops working immediately) — update the value your receiver reads it from.
The destination went quiet and now shows as broken While you were getting the verifier right, your receiver refused five notices in a row, so InfraInbox stopped sending there (see Signed webhook). Answer 2xx for anything you accept — even while you’re only logging — then Send test to bring the destination back.
  • Signed webhook — headers, retry behavior and the full payload shape.
  • REST API — if you’re building against InfraInbox’s own API rather than just receiving from it.