Webhooks

Webhooks

Recibe cada cambio de estado en tu servidor, firmado con HMAC.

Cozter notifica a tu servidor cada vez que una guía cambia de estado. En lugar de consultar la API repetidamente, deja que Cozter te avise.

Registro

Un administrador registra la URL de tu endpoint (debe ser https://) desde la configuración del comercio. Al crearla, Cozter genera un secreto de firma (whsec_…) que se muestra una sola vez — guárdalo para verificar las firmas.

Carga útil

Cozter envía un POST con este cuerpo. El campo estado usa el vocabulario de estados. En entregado se incluye la prueba de entrega con enlaces firmados frescos.

{
  "id": "3b1e…",
  "evento": "guia.estado",
  "estado": "entregado",
  "guia": "WA-000100",
  "external_ref": "PED-1023",
  "fecha": "2026-07-21T18:30:05Z",
  "datos": {
    "destinatario": "Juan Pérez",
    "telefono": "+50370000000",
    "departamento": "San Salvador",
    "municipio": "Soyapango",
    "cod_amount": 25,
    "pod": {
      "entregado_en": "2026-07-21T18:30:00Z",
      "recibido_por": "Juan Pérez",
      "fotos": ["https://…firmado…"],
      "firma": "https://…firmado…",
      "pdf": "https://…firmado…/pod.pdf"
    }
  }
}

En estado fallido, datos incluye motivo en vez de pod. En estado incidencia, datos incluye motivo (accidente, cod_no_cobrado u otro) y detalle.

Encabezados

EncabezadoDescripción
X-Cozter-Event-IdID único del evento. Úsalo para deduplicar reintentos.
X-Cozter-TimestampUnix en segundos, usado en la firma.
X-Cozter-Signaturesha256=<hex> — HMAC-SHA256 del cuerpo.

Verificar la firma

Calcula HMAC-SHA256(secreto, "{timestamp}.{cuerpo_crudo}") y compara con el encabezado X-Cozter-Signature. Usa el cuerpo tal cual llega, sin re-serializar el JSON.

verificar-webhook.ts
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verificarWebhook(req, secreto: string): boolean {
  const ts = req.headers['x-cozter-timestamp']
  const firma = req.headers['x-cozter-signature']            // "sha256=…"
  const esperado
    = 'sha256=' + createHmac('sha256', secreto)
      .update(ts + '.' + req.rawBody)                        // cuerpo crudo
      .digest('hex')
  return timingSafeEqual(Buffer.from(firma), Buffer.from(esperado))
}

Reintentos

Responde 2xx para confirmar la entrega. Ante fallo o timeout (10 s), Cozter reintenta con backoff creciente:

1 min → 5 min → 30 min → 2 h → 6 h → 24 h

Tras 6 intentos fallidos, el evento se marca como fallido y no se reintenta más. Un endpoint caído nunca bloquea la entrega de los demás eventos.

Verifica el timestamp para rechazar eventos muy viejos y usa X-Cozter-Event-Id para que un reintento no procese dos veces la misma actualización.