Estado · webhooks

Recibe cada incidente en tu endpoint.

POST JSON firmado en cada cambio. Encaja con tu stack de observabilidad.

Suscribirse

Eventos emitidos

Tres tipos cubren el ciclo de vida del incidente.

  • incident.openedUn servicio pasa a degradado, caído o mantenimiento.
  • incident.updatedNota pública añadida por el equipo durante el incidente.
  • incident.resolvedLos servicios vuelven a estar operativos.

Cabeceras HTTP

Toda solicitud lleva las siguientes cabeceras.

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

Forma del payload

Objeto JSON compacto.

{
  "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
}

Verificar la firma

HMAC-SHA256 sobre `timestamp.body`. Comparación en tiempo constante. Rechaza si el drift supera 5 min.

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)

Canal Slack

Si suscribes una URL de hooks.slack.com, recibes Block Kit:

  • Cabecera coloreada por severidad.
  • Severidad + estado.
  • Resumen + última actualización.
  • Botón al incidente.

Entrega y reintento

Se espera 2xx en 6s. Sin reintento automático.

2xx · 6 s · no retry