Signed HTTPS requests for every alert, on every paid plan. Verify them, route them and automate on them, in any language.
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.
Use the Send test button to fire a webhook.ping event at your endpoint any time.
| 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” |
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.
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.
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"
}
}
}
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"
}
}
}
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.
The Uptimesam-Signature header looks like:
Uptimesam-Signature: t=1713321600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
To verify:
t= (Unix timestamp) and v1= (hex signature) "{timestamp}.{raw_request_body}" HMAC-SHA256(signing_secret, signed_payload) and hex-encode it v1 using a constant-time comparison 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.
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")
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
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
Uptimesam-Event-Id, so deduplicate on it. webhook.ping on demand
Email us at [email protected] and include the Uptimesam-Event-Id of a sample delivery, so we can look up what we sent.