Signed webhook
A webhook destination POSTs signed JSON to a URL you control, one request per notice. Use it to wire InfraInbox into something with no adapter of its own — Home Assistant, n8n, a status page, your own service.
Add the destination
Section titled “Add the destination”- Go to Add destination → Webhook.
- Name it and enter your receiver’s URL (
https, orhttponly to a private address). - InfraInbox generates a signing secret in your browser and shows it once, shaped
whsec_…. Copy it — your receiver needs it to verify what it gets, and InfraInbox can’t show it to you again. - Save, then click Send test. Your receiver should get one request with
"type": "test".
The request
Section titled “The request”POST /your/path HTTP/1.1Content-Type: application/jsonwebhook-id: dlv_01m28xg2pdexgbwj94sksn3kc2webhook-timestamp: 1758268800webhook-signature: v1,K5g0N2vqYFvXe3wq7z6z9J3m8s2q1p9r0c4h1x8v0m0=
{"type":"incident.opened","timestamp":"2026-09-19T08:00:00Z","data":{"v":1,"title":"Backup failed","severity":"CRITICAL","state":"open","sourceName":"Proxmox Home","resource":"job backup-daily","workspaceName":"Home lab","incidentId":"inc_2f9","firstSeenAt":"2026-09-12T21:31:04Z","url":"https://infrainbox.example.com/incidents/inc_2f9","reason":"opened"}}webhook-id is the delivery’s own ID and stays the same across retries of the same attempt, so your receiver can drop duplicates by it. type is incident.<reason> for a lifecycle notice (incident.opened, incident.reoccurred, incident.escalated, incident.reopened, incident.resolved, incident.renotify, incident.fallback, incident.flapping, incident.silence_expired, incident.snooze_expired, incident.informational), incident.acknowledged, incident.snoozed or incident.updated for a state change made elsewhere, and test for a manual test send.
Verifying the signature
Section titled “Verifying the signature”webhook-signature follows the Standard Webhooks scheme: v1, followed by the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the bytes your secret decodes to.
- Strip the
whsec_prefix from your secret and base64-decode the rest — that’s the raw HMAC key. - Build the signed content: the
webhook-idheader value, a literal., thewebhook-timestampheader value, another literal., then the exact raw request body bytes (don’t re-serialize the JSON — use the bytes as received). - Compute HMAC-SHA256 of that content with the key from step 1, base64-encode the result, and prefix it with
v1,. - Compare that to the
webhook-signatureheader using a constant-time comparison.
import hashlibimport hmacimport 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() return hmac.compare_digest(expected, signature_header)const crypto = require("crypto");
function verify(secret, webhookId, timestamp, body, signatureHeader) { const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const signedContent = Buffer.concat([Buffer.from(`${webhookId}.${timestamp}.`), body]); const expected = "v1," + crypto.createHmac("sha256", key).update(signedContent).digest("base64"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));}How retries and failures are treated
Section titled “How retries and failures are treated”| Your receiver answers | InfraInbox does |
|---|---|
| 2xx | Marks it sent |
| 401, 403, 404, 410 | Stops sending here until you edit or test the destination again — your receiver is telling it to stop |
| 408, 429, 5xx | Retries with backoff |
| Any other 4xx | Gives up on this one event, but keeps sending future ones. Five in a row with nothing sent in between, and the destination stops being tried, exactly as for a 401 — a receiver that refuses every notice is a configuration to fix, not a queue to burn. One notice that goes through clears the count |
| A redirect | Never followed; treated as a failure |
What edits and state changes look like
Section titled “What edits and state changes look like”A webhook destination is one of the channels that can represent a state change: an acknowledgement, a snooze or a manual resolve made elsewhere arrives as its own incident.acknowledged, incident.snoozed or incident.updated request, rather than being skipped — there’s no message to edit, so InfraInbox just tells you what changed.
Options
Section titled “Options”- Send recovery and state updates — off, and this receiver is only told when something breaks.
- Enabled — off holds the destination without deleting it.
- Quiet-hours threshold — the severity floor this destination still hears during quiet hours.
Rotating the signing secret
Section titled “Rotating the signing secret”Click Generate a new secret on the destination’s edit form, save, and update your receiver with the new value at the same time — the old secret stops verifying immediately once you save.
- Verify a signed webhook — a worked example receiver
- Routing rules — send specific incidents here