API

API: webhook payloads

The JSON Tenium sends to webhook contacts when a monitor goes down, recovers or warns about an expiry, with headers and signature verification.

Webhook contacts receive a POST with Content-Type: application/json for each event. The body is the same alert payload Tenium renders for e-mail and chat channels.

Events

The event field (also sent in the X-Tenium-Event header) is one of:

  • down: a monitor was confirmed down
  • blocked: the checker is being challenged by a WAF or bot protection; the site itself is probably fine
  • up: the monitor recovered (sent only to contacts that received the down alert)
  • degraded: response time above the threshold, or page content changed
  • heartbeat_missed: a heartbeat did not ping on time
  • ssl_expiry: the SSL certificate expires soon or has expired
  • domain_expiry: the domain expires soon or has expired
  • test: the test alert sent from the contact page

Payload

{
  "version": 1,
  "event": "down",
  "kind": "down",
  "title": "DOWN",
  "summary": "Client A shop · 503 Service Unavailable · confirmed from Frankfurt (FRA), London (LON), New York (NYC)",
  "color": "red",
  "sentAt": "2026-01-19T09:39:14.000Z",
  "region": { "code": "fra", "name": "Frankfurt" },
  "account": { "id": "0f8e...", "name": "Your Studio", "brandName": null, "brandEmailFrom": null, "brandEmailVerifiedAt": null, "timezone": "Europe/Tallinn" },
  "client": { "id": "9a8b...", "name": "Client A" },
  "monitor": {
    "id": "a3f1...",
    "name": "Client A shop",
    "type": "http",
    "target": "https://shop.clienta.com",
    "url": "https://app.tenium.app/app/monitors/a3f1..."
  },
  "incident": {
    "id": "e5b4...",
    "severity": "down",
    "cause": "503 Service Unavailable",
    "statusCode": 503,
    "startedAt": "2026-01-19T09:39:12.000Z",
    "resolvedAt": null,
    "durationSec": 2,
    "confirmedRegions": ["fra", "lon", "nyc"],
    "acknowledgedAt": null,
    "upstreamProvider": null,
    "lastResponse": null,
    "url": "https://app.tenium.app/app/incidents/e5b4...",
    "ackUrl": "https://app.tenium.app/app/incidents/e5b4...?action=ack"
  },
  "expiry": null,
  "nextStep": "Check the server / hosting panel. Acknowledge so the team knows you are on it."
}

kind is the internal alert kind and equals event except for blocked, which arrives as kind: "down". For up, resolvedAt and durationSec are set. For expiry events incident is null and expiry is set instead: { "what": "SSL certificate", "expiresAt": "...", "daysLeft": 7, "issuer": "..." }. client is null for monitors outside a client workspace.

Headers

Every request includes:

  • X-Tenium-Event: the event name, for example down
  • X-Tenium-Delivery: a unique ID for this delivery
  • X-Tenium-Timestamp: the sentAt time of the payload
  • X-Tenium-Signature: present when the contact has a signing secret

Signature

X-Tenium-Signature: t=1737279554,v1=5f3a...

t is the Unix time in seconds when the request was signed. v1 is HMAC-SHA256 of "{t}.{rawBody}" using the contact’s signing secret (shown when the contact is created). Reject requests older than 5 minutes.

Node example:

import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Retries

Answer with any 2xx within 10 seconds. Network errors, timeouts, 5xx and 429 answers are retried: up to 3 attempts in total, 30 seconds apart. Other 4xx answers are not retried.