Webhooks
Set or replace the webhook on a monitor
Section titled “Set or replace the webhook on a monitor”Webhooks are scoped to a single monitor — each monitor exposes one webhook slot. Use PUT /api/v1/monitors/{monitorID}/webhook to set it or replace its URL:
curl -X PUT https://api.uptimemonitoring.com/api/v1/monitors/1287/webhook \ -H "Authorization: Bearer $UPTIMEMONITORING_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url":"https://hooks.example.com/incident"}'Response:
{ "id": 42, "monitor_id": 1287, "url": "https://hooks.example.com/incident", "secret": "whs_live_…", "created_at": "2026-05-19T12:00:00Z"}The response includes the webhook record and the plaintext HMAC secret. The same shape is returned on subsequent GET reads — the API does NOT redact the secret on read, so the endpoint must be treated as a credential surface (anyone who can authenticate to the account can recover the signing key).
Replacing the URL on an existing webhook keeps the existing secret. To rotate the secret, DELETE the webhook then PUT it again.
Inspect or delete a webhook
Section titled “Inspect or delete a webhook”# Read the current webhook for a monitor (returns the plaintext secret too)curl https://api.uptimemonitoring.com/api/v1/monitors/1287/webhook \ -H "Authorization: Bearer $UPTIMEMONITORING_API_KEY"
# Remove the webhook (monitor stays; just no notifications)curl -X DELETE https://api.uptimemonitoring.com/api/v1/monitors/1287/webhook \ -H "Authorization: Bearer $UPTIMEMONITORING_API_KEY"Payload format
Section titled “Payload format”{ "event": "monitor.down", "monitor_id": 30, "monitor_name": "myapp-healthz", "monitor_url": "https://example.com/health", "account_id": 1, "occurred_at": "2026-04-12T14:23:11Z", "reason": "http_5xx", "delivery_id": 3, "attempt": 1}Events: monitor.down, monitor.up, monitor.flapping.
| Field | Type | Description |
|---|---|---|
event | string | One of monitor.down, monitor.up, monitor.flapping. |
monitor_id | integer | ID of the monitor that changed state. |
monitor_name | string | Display name of the monitor. |
monitor_url | string | omitted | The URL being monitored. Omitted if the monitor has no URL. |
account_id | integer | Account that owns the monitor. |
occurred_at | RFC3339 string | When the state transition was detected. |
reason | string | omitted | Error class for monitor.down (e.g. http_5xx, timeout), flap reason for monitor.flapping, empty/omitted for monitor.up. |
delivery_id | integer | Stable per logical state transition. Use this field to deduplicate retried deliveries — the same transition always carries the same delivery_id. |
attempt | integer (1–3) | Which delivery attempt this is (1 = first try). |
Signature verification
Section titled “Signature verification”Every webhook delivery includes an X-UptimeMonitoring-Signature header containing an HMAC-SHA256 hex digest. To verify it, compute the HMAC over the raw request bytes (not a re-serialized parse), then compare with a constant-time function. Using a regular string comparison (=== / !==) leaks timing information that lets an attacker recover the expected signature byte-by-byte.
const crypto = require('crypto');const express = require('express');const app = express();
// Register this route BEFORE any global express.json() or body-parser middleware.// Earlier middleware consumes the stream first; express.raw() must run here, not after.app.post( '/webhooks/uptimemonitoring', express.raw({ type: 'application/json' }), (req, res) => { if (!Buffer.isBuffer(req.body)) { return res.status(400).send('Expected raw body — check middleware order'); } const sig = req.header('X-UptimeMonitoring-Signature') || ''; if (!/^[a-f0-9]{64}$/i.test(sig)) { return res.status(401).send('Invalid signature'); }
const expected = crypto .createHmac('sha256', process.env.UPTIMEMONITORING_WEBHOOK_SECRET) .update(req.body) // req.body is a Buffer here .digest('hex');
const a = Buffer.from(sig, 'hex'); const b = Buffer.from(expected, 'hex'); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(401).send('Invalid signature'); }
const event = JSON.parse(req.body.toString('utf8')); // …handle event.event === 'monitor.down' / 'monitor.up' / 'monitor.flapping'… res.status(204).end(); },);
app.listen(3000);import hmacimport hashlibimport osfrom flask import Flask, request, abort
app = Flask(__name__)SECRET = os.environ["UPTIMEMONITORING_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/uptimemonitoring")def receive(): raw = request.get_data() # bytes, before any parsing sig = request.headers.get("X-UptimeMonitoring-Signature", "") expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, expected): abort(401) # event = json.loads(raw) return "", 204
if __name__ == "__main__": app.run(port=3000)Background: OWASP — Observable Timing Discrepancy (CWE-208).
Retry policy
Section titled “Retry policy”Failed deliveries (non-2xx response or timeout) are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the delivery is marked as failed.
Delivery log (account-wide)
Section titled “Delivery log (account-wide)”The delivery log is exposed at the account scope, not per-webhook. Filter with monitor_id if you only want one monitor’s deliveries:
curl "https://api.uptimemonitoring.com/api/v1/webhook-deliveries?monitor_id=1287&limit=20" \ -H "Authorization: Bearer $UPTIMEMONITORING_API_KEY"| Query | Type | Description |
|---|---|---|
monitor_id | integer | Restrict to deliveries for one monitor. |
since | RFC3339 datetime | Only deliveries dispatched at or after this time. |
status | string | Filter by delivery status: pending, retrying, delivered, or failed. |
limit | integer (1–100) | Page size. Default 50. |
offset | integer | Skip the first N results. |
Response shape:
{ "deliveries": [ { "id": 9001, "webhook_id": 42, "monitor_id": 1287, "account_id": 14, "event_type": "monitor.down", "status": "delivered", "attempt_count": 1, "last_response_code": 200, "last_attempt_at": "2026-05-07T09:14:05Z", "next_attempt_at": "2026-05-07T09:14:05Z", "created_at": "2026-05-07T09:14:02Z" } ], "total": 1, "limit": 50, "offset": 0}Each delivery record includes: id, webhook_id, monitor_id, event_type, status (pending|retrying|delivered|failed), attempt_count, last_response_code, last_attempt_at, next_attempt_at, and created_at. There is no per-webhook delivery endpoint — GET /api/v1/webhooks/{id}/deliveries does not exist.