boltZharp Docs
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.

eventWhen
downA monitor went down and an incident opened.
upThe monitor recovered and the incident resolved.
degradedA certificate or domain close to expiry.
resolvedNo longer sent: it meant a partial outage cleared, and partial outages don't alert any more. A recovery is up.
acknowledgedSomeone acknowledged the incident.
maintenanceA maintenance window started.
testSent by “Send test event” on the endpoint’s page.

Headers

HeaderValue
Content-Typeapplication/json
User-AgentZharp-Webhooks/1.0
X-Zharp-EventThe event — down, up, degraded, resolved, acknowledged, maintenance or test.
X-Zharp-DeliveryA unique id for the delivery. Retries reuse it, so you can drop duplicates.
X-Zharp-Signaturet=<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 2xx to 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”.