Integrations

Webhook alerts, signed with HMAC-SHA256

For your own tools: BBoxProbe posts each alert to your HTTPS endpoint as JSON, signed with a secret only you and BBoxProbe know, in the X-BBoxProbe-Signature header.

On this page
  1. The request
  2. The payload
  3. Verify the signature
  4. Set it up
  5. When your endpoint can't take it

The request

  • A POST to the URL you give, which must start with https:// and have a public address (not a private or local network).
  • Content-Type: application/json, with the alert as the body.
  • X-BBoxProbe-Signature: sha256= followed by the HMAC-SHA256 of the raw body, in hex, keyed with the channel's signing secret.

Answer with any 2xx status within 10 seconds. Anything else counts as failed, and the alert is tried again 1, 5 and 15 minutes later, then sent to your account email instead.

The payload

This is the body of a down alert, shown with line breaks; the real body is compact JSON on one line. A closing notice has "status": "resolved" and an ended_at.

{
  "monitor": "ct3skrhd11sm",
  "name": "Website",
  "target": "https://example.com/",
  "current_target": "https://example.com/",
  "alert": "MonitorDown",
  "status": "firing",
  "started_at": "2026-10-27T10:32:00Z",
  "ended_at": null,
  "failing_cities": [
    "fra",
    "iad"
  ],
  "summary": "Website is down",
  "details": "Frankfurt and Virginia get 502 Bad Gateway.\nDown since 10:32 UTC.\nAnother notice follows when it's back.",
  "facts": {
    "held_while_unstable": false,
    "cities": [
      {
        "city": "fra",
        "name": "Frankfurt",
        "problem": "gets 502 Bad Gateway",
        "http_status": 502,
        "seconds": 0.21
      },
      {
        "city": "iad",
        "name": "Virginia",
        "problem": "gets 502 Bad Gateway",
        "http_status": 502,
        "seconds": 0.18
      }
    ]
  },
  "test": false
}
FieldWhat it holds
monitorThe monitor's id, as in its page's address.
nameThe monitor's name.
targetThe address the alert is about: for a closing notice, the one it had when the outage began.
current_targetThe monitor's address now; differs from target when it was edited since.
alertWhich alert: MonitorDown, MonitorFlapping, AvailabilityBurnFast, AvailabilityBurnSlow, LatencyDegraded, CertExpiring, CertExpiringSoon, or Test.
statusfiring or resolved.
started_at, ended_atWhen it began and ended, ISO 8601 in UTC; ended_at is null while it goes on.
failing_citiesWhile a down alert fires, the codes of the cities that fail; else empty.
summaryThe alert's title, as the email's subject says it.
detailsThe alert's text: the facts, what it means and what to do, lines joined by \n.
factsThe numbers behind it, by alert (below). A fact that isn't known is null.
testtrue on a test from Send a test, which has "alert": "Test" and no monitor.

The facts, by alert

AlertFacts
MonitorDownFiring: cities, each with city, name, problem, http_status and seconds; held_while_unstable. Resolved: down_seconds.
MonitorFlappingFiring: times_down_2h. Resolved: unstable_seconds, still_down, down_notice_at.
AvailabilityBurnFast, AvailabilityBurnSlowwindow ("1h", "6h"), availability, objective, budget_minutes, budget_used, budget_alert_above, budget_left. Shares are fractions of 1.
LatencyDegradedwindow ("30m" firing, "5m" resolved), threshold_ms, slow_share; firing also slow_cities, each with city, name, seconds.
CertExpiring, CertExpiringSoonhostname, expires_at, days_left.

This is the current format. Read the fields you need and ignore the others, so a field added later doesn't break your code.

Verify the signature

When you add the channel, the app shows its signing secret once; later, Reveal on the channel's card shows it again. Compute the HMAC-SHA256 of the raw request body with that secret, put sha256= in front, and compare it with the X-BBoxProbe-Signature header in constant time. Ignore any request where the two differ. Compute it over the bytes exactly as they arrived: parsing the JSON and encoding it again changes them.

Ruby

# Rails: request.raw_post is the body exactly as it arrived
expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, request.raw_post)
Rack::Utils.secure_compare(expected, request.headers["X-BBoxProbe-Signature"].to_s)

Node

// Express: express.raw({ type: "application/json" }) keeps the body as bytes
const crypto = require("node:crypto")
const expected = Buffer.from("sha256=" + crypto.createHmac("sha256", secret).update(req.body).digest("hex"))
const given = Buffer.from(req.get("X-BBoxProbe-Signature") || "")
const valid = given.length === expected.length && crypto.timingSafeEqual(expected, given)

Python

import hashlib, hmac

# body: the raw request body as bytes; signature_header: the header's value
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
valid = hmac.compare_digest(expected, signature_header)

Set it up

  1. In BBoxProbe, open Alert channels and, under Add an alert channel, pick Webhook as the kind.
  2. Give it a Name, such as "On-call", and paste the URL into Webhook URL (https://, a public address).
  3. Press Add channel, then Send a test on its card. The test arrives as "Test from BBoxProbe: your webhook channel 'On-call' works".
  4. Alerts now go there from every monitor that hasn't picked its own channels. To send only some monitors' alerts there, tick it under Alert channels in those monitors' settings.

Each notice is sent once per channel. In rare failures one can arrive twice; the monitor, alert, status and started_at together name it, if you want to drop a repeat.

When your endpoint can't take it

Each alert is posted as it happens, and its closing notice when it ends. When your endpoint doesn't answer within 10 seconds or answers with an error, the alert is tried again 1, 5 and 15 minutes later. After the last try it goes to your account email instead, saying which channel couldn't take it and why. The channels page lists the last 50 alerts and tests sent, with each channel's result.

The number of alert channels depends on the plan: 1 on Free, 3 on Pro, and 10 on Max. Every kind of channel is on every plan.