Webhooks

Signed HTTPS requests for every alert, on every paid plan. Verify them, route them and automate on them, in any language.

Webhooks

Webhooks let UptimeSam push alerts to any HTTPS endpoint you control: your backend, an incident tool, an automation platform, anything that accepts a JSON POST. Every delivery is signed with HMAC-SHA256 so your endpoint can check that it really came from UptimeSam.

Using Hookfire? UptimeSam is a built-in source there: paste your Hookfire URL as the endpoint and alerts become push notifications on your phone.

1) Create a webhook

  1. Sign in to UptimeSam as a board admin
  2. Open your board, then Board Settings → Webhooks
  3. Click New Webhook
  4. Paste your endpoint URL
  5. Pick which events it should receive
  6. Save. UptimeSam generates a signing secret, shown next to the row. Click “Reveal” to view it, and copy it into your server’s environment variables. Treat it like a password.

Use the Send test button to fire a webhook.ping event at your endpoint any time.

2) Events

Event type Fires when
monitor.down A monitor fails its retries and is declared down
monitor.up A monitor that was down answers again
monitor.still_down A monitor is still down, at the reminder interval set on it
monitor.slow Three checks in a row were slower than the monitor’s slow threshold
monitor.slow_resolved A slow monitor answers within its threshold again
ssl.expiring A certificate is 14, 7 or 1 days from expiring (the first threshold follows the monitor’s setting)
domain.expiring A monitored domain’s registration is 30, 7 or 1 days from expiring
incident.created An incident is posted on the board’s status page
incident.updated An incident gets an update
webhook.ping You click “Send test”

3) Request format

Every request is an HTTPS POST with a JSON body. Headers:

Content-Type: application/json
User-Agent: Uptimesam-Webhooks/1.0
Uptimesam-Signature: t=1713321600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Uptimesam-Event-Id: evt_018f4a9b-2d3e-7c4a-8a6d-12345abc
Uptimesam-Event-Type: monitor.down
Uptimesam-Timestamp: 1713321600

Uptimesam-Event-Id is the same on every retry of one event, so use it as an idempotency key.

Example payload: monitor.down

{
  "id": "evt_018f4a9b-2d3e-7c4a-8a6d-12345abc",
  "type": "monitor.down",
  "created": 1713321600,
  "data": {
    "object": {
      "monitor": {
        "id": "019a1c9b-7386-746f-a20d-bf926fee60fd",
        "name": "API",
        "type": "http",
        "target": "https://api.example.com/health",
        "url": "https://uptimesam.com/app/board/acme/monitors/019a1c9b-7386-746f-a20d-bf926fee60fd"
      },
      "board": { "id": "019a1c9b-...", "name": "Acme", "slug": "acme" },
      "status": "down",
      "outage": { "started_at": "2026-04-17T10:29:20Z", "ended_at": null, "duration_seconds": null },
      "check": { "status_code": 503, "error": "http_503", "error_description": "HTTP 503" },
      "status_page_url": "https://acme.uptimesam.com"
    }
  }
}

outage.started_at is when the first failed check ran, before the retries confirmed it.

Example payload: monitor.up

{
  "id": "evt_...",
  "type": "monitor.up",
  "created": 1713322080,
  "data": {
    "object": {
      "monitor": { "id": "...", "name": "API", "type": "http", "target": "https://api.example.com/health", "url": "https://uptimesam.com/app/board/acme/monitors/..." },
      "board": { "id": "...", "name": "Acme", "slug": "acme" },
      "status": "up",
      "outage": { "started_at": "2026-04-17T10:29:20Z", "ended_at": "2026-04-17T10:38:00Z", "duration_seconds": 520 },
      "status_page_url": "https://acme.uptimesam.com"
    }
  }
}

Example payload: ssl.expiring

{
  "id": "evt_...",
  "type": "ssl.expiring",
  "created": 1713321600,
  "data": {
    "object": {
      "monitor": { "id": "...", "name": "Website", "type": "http", "target": "https://example.com", "url": "https://uptimesam.com/app/board/acme/monitors/..." },
      "board": { "id": "...", "name": "Acme", "slug": "acme" },
      "status": "up",
      "certificate": { "expires_at": "2026-04-24T23:59:59Z", "days_left": 7 },
      "status_page_url": "https://acme.uptimesam.com"
    }
  }
}

Example payload: incident.created

{
  "id": "evt_...",
  "type": "incident.created",
  "created": 1713321600,
  "data": {
    "object": {
      "incident": {
        "id": "...",
        "title": "Slow API responses",
        "status": "investigating",
        "status_label": "Investigating",
        "impact": "minor",
        "url": "https://acme.uptimesam.com"
      },
      "update": { "message": "We are looking into slow responses from the API.", "status": "investigating" },
      "board": { "id": "...", "name": "Acme", "slug": "acme" },
      "status_page_url": "https://acme.uptimesam.com"
    }
  }
}

Error codes in check.error include timeout, connection_refused, unresolvable_host, http_<status>, keyword_missing, keyword_found, ssl_certificate_expired, dns_no_records, dns_mismatch, no_ping and reported_failure. check.error_description says the same in words.

4) Verifying signatures

The Uptimesam-Signature header looks like:

Uptimesam-Signature: t=1713321600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

To verify:

  1. Parse out the t= (Unix timestamp) and v1= (hex signature)
  2. Build the signed payload: "{timestamp}.{raw_request_body}"
  3. Compute HMAC-SHA256(signing_secret, signed_payload) and hex-encode it
  4. Compare byte-equality against v1 using a constant-time comparison
  5. Reject the request if the timestamp is more than 5 minutes from now (replay protection)

Node.js

const crypto = require("crypto");

function verifyUptimesamSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const timestamp = parseInt(parts.t, 10);
  const signature = parts.v1;

  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) {
    throw new Error("Timestamp outside tolerance");
  }

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  if (
    signature.length !== expected.length ||
    !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  ) {
    throw new Error("Invalid signature");
  }
}

Important: pass the raw request body (as a string or Buffer), not the parsed JSON. Express users should use express.raw({ type: "application/json" }) for the webhook route.

Python

import hmac, hashlib, time

def verify_uptimesam_signature(raw_body: bytes, header: str, secret: str, tolerance=300):
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp = int(parts["t"])
    signature = parts["v1"]

    if abs(int(time.time()) - timestamp) > tolerance:
        raise ValueError("Timestamp outside tolerance")

    signed_payload = f"{timestamp}.{raw_body.decode()}".encode()
    expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise ValueError("Invalid signature")

Ruby

require "openssl"

def verify_uptimesam_signature(raw_body, header, secret, tolerance: 300)
  parts = header.split(",").map { |p| p.split("=", 2) }.to_h
  timestamp = parts["t"].to_i
  signature = parts["v1"]

  raise "Timestamp outside tolerance" if (Time.now.to_i - timestamp).abs > tolerance

  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
  raise "Invalid signature" unless Rack::Utils.secure_compare(signature, expected)
end

Elixir

def verify_uptimesam_signature(raw_body, header, secret, tolerance \\ 300) do
  parts = header |> String.split(",") |> Map.new(&List.to_tuple(String.split(&1, "=", parts: 2)))
  timestamp = String.to_integer(parts["t"])
  signature = parts["v1"]

  if abs(System.system_time(:second) - timestamp) > tolerance do
    raise "Timestamp outside tolerance"
  end

  expected =
    :crypto.mac(:hmac, :sha256, secret, "#{timestamp}.#{raw_body}")
    |> Base.encode16(case: :lower)

  unless Plug.Crypto.secure_compare(signature, expected) do
    raise "Invalid signature"
  end
end

5) Delivery

  • Your endpoint should answer within 10 seconds. Any 2xx response counts as delivered.
  • A failed delivery (a non-2xx answer, a timeout, a connection error) is retried with growing delays, up to 6 attempts in all. Every attempt shows in the webhook’s delivery log.
  • Retries carry the same Uptimesam-Event-Id, so deduplicate on it.

6) Testing locally

  • Use webhook.site to inspect raw requests
  • Use ngrok or similar to expose your local dev server
  • Use the Send test button in the admin UI to fire a webhook.ping on demand

Need help?

Email us at [email protected] and include the Uptimesam-Event-Id of a sample delivery, so we can look up what we sent.