webhookIntegrations
Webhooks
Zharp POSTs monitor and incident events to your own HTTPS endpoint as JSON, signed so you can check they came from us.
Payload
Every event is one POST with a JSON body like this:
JSON
{
"id": 4821,
"timestamp": "2026-10-12T14:03:48.388Z",
"workspaceId": 12,
"monitorId": 3381,
"monitorName": "Checkout API",
"monitorUrl": "https://api.esoko.com/checkout",
"status": "DOWN",
"event": "down",
"incident": {
"id": 902,
"title": "Checkout API is down",
"description": "Connection timed out after 30s"
}
}incident is null when the event isn't tied to one. Degraded, acknowledged and maintenance events also carry a human-readable message.
Events
Choose which ones an endpoint receives under “Events sent” on its page.
| event | When |
|---|---|
| down | A monitor went down and an incident opened. |
| up | The monitor recovered and the incident resolved. |
| degraded | A certificate or domain close to expiry. |
| resolved | No longer sent: it meant a partial outage cleared, and partial outages don't alert any more. A recovery is up. |
| acknowledged | Someone acknowledged the incident. |
| maintenance | A maintenance window started. |
| test | Sent by “Send test event” on the endpoint’s page. |
Headers
| Header | Value |
|---|---|
| Content-Type | application/json |
| User-Agent | Zharp-Webhooks/1.0 |
| X-Zharp-Event | The event — down, up, degraded, resolved, acknowledged, maintenance or test. |
| X-Zharp-Delivery | A unique id for the delivery. Retries reuse it, so you can drop duplicates. |
| X-Zharp-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> — see below. |
Verifying the signature
Each endpoint has its own signing secret (whsec_…), shown when you add it. Zharp signs the timestamp and the raw body together:
Signed string
v1 = hex( HMAC-SHA256( secret, "<t>.<raw body>" ) )Recompute it from the raw body, compare in constant time, and reject requests whose t is more than a few minutes old — that stops a captured request being replayed later.
Node.js
import crypto from "node:crypto";
// Use the raw request body — re-serialising parsed JSON changes the bytes.
export function verifyZharp(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const given = Buffer.from(parts.v1 ?? "");
return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), given);
}Python
import hmac, hashlib, time
def verify_zharp(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", 0))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Responses, timeouts and retries
- Answer with any
2xxto acknowledge the event. Anything else counts as a failure. - An endpoint that hasn't answered within 10 seconds has timed out.
- Failed deliveries are retried with exponential backoff (5s, 10s, 20s…). Set 1, 3 or 5 attempts under “Retries” on the endpoint's page — the default is 3.
- Every delivery, its status code and response time show under “Recent deliveries”.