Receive webhooks and verify their signatures

Send signed JSON alert events to your own HTTPS endpoint and verify the HMAC-SHA256 X-Monitoring-Signature header with Node.js or Python example code.

Updated By the Uptime Tracker team
  1. Prepare an HTTPS endpointCreate a publicly reachable HTTPS URL that accepts POST requests with a JSON body.
  2. Add a Generic webhook destinationIn Integrations, choose "Set up Generic webhook" and enter a Name and the Webhook URL.
  3. Create the signing secretChoose "Generate a signing secret" or paste your own, and copy it into your receiver before saving.
  4. SaveChoose "Add destination". The secret is stored encrypted and never shown again.
  5. Verify every requestRecompute the HMAC-SHA256 of "<t>.<raw body>" and compare it with v1 in the X-Monitoring-Signature header, then check the timestamp.
  6. Send a testChoose "Send test" and confirm your endpoint accepts it with a 2xx response.

A generic webhook sends each alert as a signed JSON POST to your HTTPS endpoint. Webhooks are available on every plan, including Free, and are the easiest way to connect Uptime Tracker to your own tools.

Request format

Each request has Content-Type: application/json and these headers:

  • X-Monitoring-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
  • X-Monitoring-Event-ID: the event id, for de-duplication
  • X-Monitoring-Schema: the envelope version, currently 1

The body is an event envelope:

{
  "id": "6f0c…",
  "type": "monitor.down",
  "occurred_at": "2026-10-05T09:14:03Z",
  "data": {
    "incident_id": "…",
    "monitor_id": "…",
    "monitor_name": "Checkout API",
    "monitor_kind": "http",
    "target": "api.example.com",
    "category": "availability",
    "opened_at": "2026-10-05T09:13:31Z",
    "resolved_at": null
  }
}

Common event types are monitor.down and monitor.recovered. Ignore types you do not handle.

How the signature works

The signature is the hex HMAC-SHA256, keyed with your signing secret, of the timestamp, a dot, and the exact raw request body: <t>.<body>. Verify it against the raw bytes before parsing JSON, compare in constant time, and reject timestamps outside your replay window (5 minutes is a sensible default).

Node.js (Express)

const crypto = require('crypto');
const express = require('express');

const app = express();
const SECRET = process.env.UPTIME_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;

app.post('/hooks/uptime', express.raw({ type: 'application/json' }), (req, res) => {
  const parts = {};
  for (const piece of (req.get('X-Monitoring-Signature') || '').split(',')) {
    const i = piece.indexOf('=');
    if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
  }
  const t = Number(parts.t);
  if (!parts.v1 || !Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) {
    return res.status(400).send('invalid signature');
  }
  const expected = crypto.createHmac('sha256', SECRET)
    .update(parts.t + '.')
    .update(req.body) // raw Buffer
    .digest('hex');
  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(parts.v1, 'utf8');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(400).send('invalid signature');
  }
  const event = JSON.parse(req.body.toString('utf8'));
  // Use event.id to ignore duplicates, then handle event.type.
  res.sendStatus(204);
});

app.listen(3000);

Python (Flask)

import hashlib, hmac, json, os, time
from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["UPTIME_WEBHOOK_SECRET"].encode()
TOLERANCE_SECONDS = 300

@app.post("/hooks/uptime")
def uptime_hook():
    header = request.headers.get("X-Monitoring-Signature", "")
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
        signature = parts["v1"]
    except (KeyError, ValueError):
        abort(400)
    if abs(time.time() - t) > TOLERANCE_SECONDS:
        abort(400)
    body = request.get_data()  # raw bytes, before any JSON parsing
    expected = hmac.new(SECRET, parts["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature):
        abort(400)
    event = json.loads(body)
    # Use event["id"] to ignore duplicates, then handle event["type"].
    return "", 204

Delivery and retries

Respond with any 2xx status within 15 seconds. Redirects are not followed. Failed deliveries are retried with backoff (about 5 seconds, 30 seconds, 2 minutes, 10 minutes and 30 minutes), and a Retry-After header is honored. Because a retry can repeat an event you already processed, de-duplicate by event id. If your endpoint keeps returning a permanent error, the destination is disabled with a reason and the workspace is told through its other destinations. The Deliveries tab shows every attempt.

FAQ

Frequently asked questions

Which header carries the webhook signature?

X-Monitoring-Signature, formatted as t=,v1=.">, keyed with your signing secret.

Can I see the signing secret again?

No. It is stored encrypted and never shown again. Copy it into your receiver before saving; to rotate it, create a new destination.

Can webhooks go to an internal URL?

No. The webhook URL must be a publicly reachable HTTPS endpoint. Private and internal addresses are refused.

Why did I receive the same event twice?

A retry can repeat an event if your acknowledgement was lost. Use the event id (also in X-Monitoring-Event-ID) to ignore duplicates.

Uptime Tracker

Start monitoring in under five minutes

Start on the free plan — commercial use allowed. No credit card, no password, just your email address.

  • Free forever plan
  • No credit card
  • Cancel anytime