pingrobot

PingRobot / Docs

Webhooks

PingRobot can POST a JSON event to your URL whenever a monitor goes down or comes back up. Every request is signed so you can check it really came from us.

Set up

  1. In the app, open Alert contacts and add a Webhook with an https:// URL.
  2. Copy the signing secret (whsec_…) shown on the contact. Keep it on your server only.
  3. Press Send test to receive a sample event marked "test": true.

New webhooks are attached to all your monitors; you can change that per monitor on its detail page.

When three or more monitors go down (or recover) in the same check, email, Slack, Discord and ntfy contacts get one summary message. Webhooks still receive one event per monitor, so your code doesn't need to handle a batch format.

The request

POST to your URL with a JSON body:

{
  "id": "evt_3f2a9c0d6b1e4f7a8c5d2e1b0a9f8e7d",
  "event": "monitor.down",
  "test": false,
  "monitor": { "id": 12, "name": "Shop", "type": "http", "target": "https://shop.example.com/" },
  "reason": "HTTP 503 Service Unavailable",
  "at": "2026-10-15T09:12:00.000Z",
  "downSince": null,
  "url": "https://pingrobot.io/app/monitors/12"
}
FieldMeaning
idUnique event id. Use it to ignore duplicates (a retry sends the same event again).
eventmonitor.down, monitor.still_down (a repeat reminder), monitor.up, ssl.expiring, domain.expiring, ssl.problem, ssl.resolved or ssl.issuer_changed (see certificate and domain events).
testtrue for events sent with Send test.
monitorThe monitor's id, name, type (http, keyword, port, heartbeat, dns) and target.
reasonWhy it is down, e.g. HTTP 503 Service Unavailable or Timed out. null on monitor.up.
atWhen the change was confirmed (ISO 8601, UTC).
downSinceOn monitor.up, and on monitor.down sent after an alert delay: when the outage started.
urlThe monitor in the PingRobot app.

Alert delay

A monitor can wait before announcing an outage ("Send the down alert after 5 minutes"). If it recovers sooner, no monitor.down and no monitor.up is sent, though the outage still counts in uptime. If it is still down after the delay, monitor.down arrives with downSince set to when the outage started.

With Remind me while it's down set, monitor.still_down is sent every chosen interval until the monitor recovers, with downSince set. The recovery is still a single monitor.up.

Certificate and domain events

When a monitor has SSL or domain reminders turned on, you also get ssl.expiring and domain.expiring events: once when the expiry date enters the reminder window, then at most weekly, and daily in the last three days. reason is a one-line summary, and one extra object carries the details:

{
  "event": "ssl.expiring",
  "reason": "The SSL certificate of shop.example.com expires on 2026-10-30 (in 10 days)",
  "certificate": { "host": "shop.example.com", "issuer": "Let's Encrypt (R11)", "expiresAt": "2026-10-30T12:00:00.000Z", "daysLeft": 10 },
  ...
}

{
  "event": "domain.expiring",
  "reason": "example.com expires on 2026-11-20 (in 30 days)",
  "domain": { "name": "example.com", "expiresAt": "2026-11-20T04:00:00.000Z", "daysLeft": 30 },
  ...
}

The daily certificate read can also send ssl.problem when a problem appears (certificate.problem is name_mismatch, self_signed or incomplete_chain), ssl.resolved when it goes away, and ssl.issuer_changed when the issuing organisation changes (certificate.previousIssuer has the old one). Routine renewals by the same certificate authority don't trigger it.

These events don't change the monitor's up/down status. Expiry reminders stop on their own once the certificate or domain is renewed.

Headers

HeaderValue
X-PingRobot-EventSame as event.
X-PingRobot-DeliverySame as id.
X-PingRobot-TimestampUnix time (seconds) when the request was signed.
X-PingRobot-Signaturev1= followed by the hex HMAC-SHA256 of <timestamp>.<raw body> with your signing secret.
User-AgentPingRobot-Webhook/1.0 (+https://pingrobot.io/docs/webhooks)

Verify the signature

Compute the HMAC over the raw request body exactly as received (before parsing JSON), compare it in constant time, and reject old timestamps to stop replays.

Node.js

import crypto from "node:crypto";

// rawBody: the request body as a string, before JSON.parse
export function verifyPingRobot(rawBody, headers, secret) {
  const ts = headers["x-pingrobot-timestamp"];
  const sig = headers["x-pingrobot-signature"] ?? "";
  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = "v1=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Python

import hashlib, hmac, time

def verify_pingrobot(raw_body: bytes, headers, secret: str) -> bool:
    ts = headers.get("X-PingRobot-Timestamp", "")
    sig = headers.get("X-PingRobot-Signature", "")
    if not ts or abs(time.time() - int(ts)) > 300:
        return False
    expected = "v1=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expected)

Delivery and retries

  • Reply with any 2xx status within 10 seconds. Do slow work after responding.
  • If your endpoint times out, can't be reached, or answers 429 or 5xx, we retry once after about 2 seconds with the same id.
  • Other 4xx answers are not retried.
  • Requests come from Cloudflare's network, so there is no fixed IP list to allow. Use the signature instead.
  • Only https:// URLs on the public internet are accepted.

If your secret leaks

Delete the webhook contact and add it again; the new contact gets a new secret.

Questions? Email hello@pingrobot.io.