Status · webhooks

Receive every incident on your endpoint.

A signed JSON POST on every incident open, update and resolve. Direct fit for your observability stack, no mail-client dependency.

Subscribe

Emitted events

Three types cover an incident's lifecycle. No heartbeat, no marketing event, only real status changes.

  • incident.openedA service moved to degraded, down, or maintenance.
  • incident.updatedPublic note added by the team during the incident.
  • incident.resolvedAffected services are back to operational.

HTTP headers

Every request arrives with these headers. The signature header is only present when a signing key is configured.

POST /your/endpoint HTTP/1.1
Host: ops.example.com
Content-Type: application/json
User-Agent: Coffrify-Status-Webhook/1.0
X-Coffrify-Event: incident.opened
X-Coffrify-Timestamp: 1717248000
X-Coffrify-Signature: t=1717248000,v1=5b8c…f3a2

Payload shape

The body is a compact JSON object. The message field carries the latest team comment (can be null at open).

{
  "type": "incident.opened",
  "ts": "1717248000",
  "incident": {
    "id": "8c6a0c5e-…",
    "slug": "eu-west-uploads-degraded",
    "title": "Uploads dégradés en région eu-west",
    "severity": "major",
    "summary": "Latence accrue sur les uploads multipart depuis ~ 14:22 UTC.",
    "affected_services": ["uploads", "api"],
    "url": "https://status.coffrify.com/incident/eu-west-uploads-degraded"
  },
  "message": null
}

Verifying the signature

HMAC-SHA256 over `timestamp.body` with the shared key, hex-encoded. Constant-time comparison against the v1 part of the header. Reject if the timestamp drifts by more than 5 minutes (anti-replay).

Node.js / TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, header: string, secret: string): boolean {
  const m = /t=(\d+),v1=([0-9a-f]+)/.exec(header);
  if (!m) return false;
  const [, ts, sig] = m;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(sig, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}
Python
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts, sig = parts.get("t"), parts.get("v1")
    if not ts or not sig: return False
    if abs(time.time() - int(ts)) > 300: return False
    expected = hmac.new(
        secret.encode(),
        f"{ts}.{raw_body.decode()}".encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, sig)

Slack channel

Subscribing a hooks.slack.com URL gives you a Block Kit message instead, built for in-channel scanning:

  • Header tinted by severity.
  • Severity + status in two monospace columns.
  • Incident summary + latest update.
  • “View incident” button.

Delivery & retry

Coffrify expects a 2xx response within 6 seconds. No automatic retry today: an unavailable endpoint misses the event. Watch your own uptime and backfill via /incidents.

2xx · 6 s · no retry