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