Skip to content

Signed webhook

The signed webhook connects Perstat to receivers of your own: an incident tool that accepts generic alert sources, an automation you run yourself, or a script. Chat destinations, PagerDuty, and Opsgenie have dedicated connectors of their own.

Under Integrations in the app, create a connector of the kind Webhook and give it a URL (owners and admins). Perstat generates a signing secret and shows it exactly once. Afterwards it appears masked, and you can rotate it at any time. Rotation replaces the secret immediately and the old one stops verifying at once, so update your receiver in the same step.

With webhooks:write, the REST API creates, updates, tests, and deletes webhooks and rotates their secrets, so your provisioning scripts can manage them too.

The secret has the form whsec_ followed by 64 lowercase hexadecimal characters. Treat the whole string as an opaque key.

http:// URLs are accepted for local receivers. Use https:// for anything that crosses a network you do not own. The target must be publicly routable: a host that resolves to a private, loopback, or link-local address is refused at every send.

What an endpoint receives depends on whether your organization has an active escalation chain: a plan with the on-call rotation (Sentinel and up, see plans and limits), on-call switched on, and the chain enabled.

Without an active chain (the default on every other plan, or with on-call or the chain switched off), every active endpoint receives the events it subscribes to under Integrations:

Event When
incident.opened An incident opens, critical or degraded. An alarm held back by a dependency rule is delivered once it is released.
incident.resolved The incident closes.
incident.test You press Test on the connector. Delivered synchronously, to this endpoint only.

With an active chain, the chain owns outgoing incident messages. An endpoint receives outage messages for critical incidents, from the steps that name it as a target, at minute 0 or later:

Event When
incident.escalation A step that names the endpoint fires. This is also the first message for a step at minute 0: the event says “this incident is unacknowledged”, not “a new one just opened”.
incident.resolved The incident closes. It goes to every endpoint a step alerted, whether or not it subscribed, and to every active endpoint subscribed to it. What the chain opens, the chain closes.
incident.test As above.

With an active chain, the subscription to incident.opened has no effect. A step skips an endpoint that is switched off, and it skips all endpoints while the incident is acknowledged, while its monitor is disabled or archived, and while the monitor sits inside a maintenance window. An endpoint switched off mid-incident still receives the all-clear for an incident it was alerted about.

One POST per event, with a JSON body:

POST <your URL>
content-type: application/json
user-agent: perstat-webhook/1.0
x-perstat-signature: sha256=<64 lowercase hex>
{
"event": "incident.escalation",
"incident": {
"id": "inc_9f3c2a7e1b4d48c0a6e5f1d2c3b4a596",
"severity": "critical",
"cause": "HTTP 503",
"summary": "API does not answer",
"monitor": { "id": "mon_4b1e8d2c7a9f4e6b8c0d1e2f3a4b5c6d", "name": "API" },
"started_at_unix": 1757930400,
"resolved_at_unix": null
},
"organization": { "id": "org_2d7a9c4e1f3b4a5c8d6e7f8a9b0c1d2e", "name": "Acme" }
}
Field Meaning
event One of the events above.
incident.id Stable across all events of one incident. Use it to open and close the same case on your side. PagerDuty receives it as dedup_key, Opsgenie as alias.
incident.severity critical or degraded. Through an active chain, you receive critical. The test sends critical too.
incident.cause Free text: the detail of the first failed check, or the cause entered on a manual incident. test on a test delivery.
incident.summary Optional, null when empty.
incident.monitor id and name of the linked monitor. null for a manual incident without a monitor and for the test.
incident.started_at_unix Unix seconds.
incident.resolved_at_unix Unix seconds, null until the incident is resolved.
organization id and name of the organization that owns the endpoint.

The body is stable and grows additively: parse what you need and ignore fields you do not know.

The header carries sha256= followed by the HMAC-SHA256 of the raw request body, keyed with the secret string exactly as it was shown to you: its ASCII bytes are the key, and it is not decoded. Compute the MAC over the bytes you received, before any JSON parsing, and compare in constant time.

Node:

import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, header) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return expected.length === header.length
&& timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}

Python:

import hashlib
import hmac
def verify(secret: str, raw_body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)

A body that fails verification did not come from Perstat, or was altered on the way. Answer it with 401 and do nothing else.

The signature covers the body alone, without a timestamp or a delivery ID, so a captured request verifies again when it is replayed. Deliver over TLS, and treat a repeated (incident.id, event) pair as already handled: for an incident tool, closing an alert twice is harmless, and opening it twice is noise.

  • A 2xx response counts as delivered. Any other status, including a 3xx, counts as failed, and redirects are not followed.
  • The timeout is 10 seconds per delivery. Answer quickly and do the work afterwards.
  • Perstat delivers each event once.
  • The outcome of the last delivery, with the error text if there was one, is shown on the connector under Integrations and beside the step in the chain.

Keep the receiver available, and reconcile missed events over the REST API: read an incident by its incident.id, or list the organization’s open incidents with incidents:read.

  • Alerting and on-call: put the endpoint into an escalation chain.
  • API: manage webhooks and read incidents with a scoped key.