Skip to content
Rowsafe
Docs

Webhooks

The JSON payload Rowsafe sends to a webhook channel, its headers, retries, and how to verify the signature.

A webhook channel sends each alert as a signed JSON POST to your https URL. Create one:

rowsafe channels add --type webhook --name pager --url https://hooks.example.com/rowsafe

It prints the signing secret (whsec_...) once. Store it where your receiver can read it.

Request

POST with Content-Type: application/json, User-Agent: Rowsafe-Notifications/1, and:

Header
X-Rowsafe-Eventalert.firing, alert.resolved, alert.reminder or test
X-Rowsafe-DeliveryA unique delivery ID (ntf_...). The same ID is reused when a delivery is retried.
X-Rowsafe-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>

Payload

{
  "version": 1,
  "event": "alert.firing",
  "delivery_id": "ntf_5k2m9x7q4t1v0",
  "sent_at": "2026-09-24T03:12:00Z",
  "org_id": "org_0123456789abc",
  "alert": {
    "id": "alr_7h3k2m9x7q4t1",
    "rule": "disk_free_critical",
    "severity": "critical",
    "state": "firing",
    "database": { "id": "db_1a2b3c4d5e6f7", "name": "app" },
    "host": { "id": "host_9h8g7f6e5d4c3", "hostname": "db-1" },
    "summary": "app: only 4.2% disk space left on db-1 (1.9 GiB free)",
    "description": "When the data directory's filesystem fills up, PostgreSQL stops accepting writes and may shut down. ...",
    "next_step": "On db-1: df -h and du -sh the data directory's pg_wal; rowsafe db protection app shows whether WAL archiving is failing",
    "value": 4.2,
    "threshold": 5,
    "unit": "%",
    "url": "https://app.rowsafe.sh/databases/app",
    "started_at": "2026-09-24T03:05:00Z"
  }
}
Field
versionPayload version, 1.
eventSame as X-Rowsafe-Event.
alert.ruleOne of the alert rules.
alert.severitycritical, warning or info.
alert.statefiring or resolved.
alert.database, alert.hostThe target, when there is one. A host alert has no database.
alert.value, threshold, unitFor rules with a threshold.
alert.resolved_at, acknowledged_at, acknowledged_byPresent once they apply.

Optional fields are left out when empty. Ignore fields you don't recognize.

Delivery

  • Answer with any 2xx within 10 seconds. Anything else, including a redirect, counts as a failure.
  • Failed deliveries are retried up to 8 times, waiting 1, 2, 4, 8, 16, 32 and 64 minutes: about 2 hours in all.
  • Use X-Rowsafe-Delivery to ignore duplicates.
  • The URL must be public https, with no credentials in it. Rowsafe refuses private and internal addresses, also after DNS resolution.
  • rowsafe channels test ID sends a test event right away.

Verify the signature

The signature is an HMAC-SHA256, keyed with the signing secret (the whole whsec_... string), over the timestamp, a dot, and the raw request body. Reject requests whose timestamp is more than 5 minutes off, to stop replays. Compare in constant time.

const crypto = require("node:crypto");

function verify(secret, header, rawBody, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Use the body exactly as received. Parsing and re-serializing the JSON changes the bytes and breaks the signature.

Slack, Discord and email

These channels get a readable message instead of JSON: the state and severity, the summary, the description, the target, the value and threshold, since when, the next step, and a link to the dashboard. Discord messages never mention anyone. Reminder emails tell you how to acknowledge the alert.

Edit on GitHub