Dokumentation
Bläddra i dokumentationen

Webhooks for analytics events

Receive signed webhooks when alerts fire, uptime changes, exports finish or sites change, and verify the HMAC-SHA256 signature in Node.js, Python or Ruby.

Visa som Markdown

Workspace settings → Webhooks → Add endpoint. Enter an https URL and choose the events. A signing secret (whsec_…) is created for the endpoint.

Events#

Event Sent when
alert.triggered An alert fired
alert.resolved An alert recovered
goal.completed_threshold A goal reached its completion threshold
uptime.down An uptime check started failing
uptime.up An uptime check recovered
export.ready An export is ready to download
import.completed An import finished
site.created A site was added
site.deleted A site was deleted
member.added A member joined the workspace
usage.threshold Usage crossed a share of the plan allowance

Send test delivers a webhook.test event.

Request#

http
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Privatus-Webhooks/1.0
Privatus-Event: uptime.down
Privatus-Delivery: whd_…
Privatus-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
json
{
  "id": "whd_…",
  "event": "uptime.down",
  "created_at": "2026-09-29T10:15:00Z",
  "workspace_id": "ws_7Q2K9XH3AB",
  "data": { "…": "event-specific fields" }
}

Respond with any 2xx within 10 seconds. Anything else, or a timeout, counts as a failure and is retried up to 8 times with exponential backoff (from about a minute to a few hours). Test deliveries aren't retried. The delivery log shows every attempt with its status and response, and you can redeliver any delivery. After 20 consecutive failures the endpoint is disabled. Fix it and re-enable it.

Verify the signature#

Privatus-Signature is t=<unix time>,v1=<hex HMAC-SHA256> where the HMAC is computed with your signing secret over "<t>.<raw request body>". Reject requests whose signature doesn't match or whose t is more than five minutes old.

js
import crypto from 'node:crypto'

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
python
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(parts["t"])) < 300 and hmac.compare_digest(expected, parts["v1"])
ruby
def verify(raw_body, header, secret)
  parts = header.split(",").to_h { |p| p.split("=", 2) }
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{parts['t']}.#{raw_body}")
  (Time.now.to_i - parts["t"].to_i).abs < 300 &&
    ActiveSupport::SecurityUtils.secure_compare(expected, parts["v1"])
end

Use the raw body as received. Re-serializing the JSON changes it.

Security#

We only deliver to public addresses (never to private or internal networks), from the User-Agent Privatus-Webhooks/1.0. Webhooks are available on Pro and above.