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
- In the app, open Alert contacts and add a Webhook with an
https://URL. - Copy the signing secret (
whsec_…) shown on the contact. Keep it on your server only. - 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"
}
| Field | Meaning |
|---|---|
id | Unique event id. Use it to ignore duplicates (a retry sends the same event again). |
event | monitor.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). |
test | true for events sent with Send test. |
monitor | The monitor's id, name, type (http, keyword, port, heartbeat, dns) and target. |
reason | Why it is down, e.g. HTTP 503 Service Unavailable or Timed out. null on monitor.up. |
at | When the change was confirmed (ISO 8601, UTC). |
downSince | On monitor.up, and on monitor.down sent after an alert delay: when the outage started. |
url | The 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
| Header | Value |
|---|---|
X-PingRobot-Event | Same as event. |
X-PingRobot-Delivery | Same as id. |
X-PingRobot-Timestamp | Unix time (seconds) when the request was signed. |
X-PingRobot-Signature | v1= followed by the hex HMAC-SHA256 of <timestamp>.<raw body> with your signing secret. |
User-Agent | PingRobot-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.