Ir al contenido

Webhooks

Recibe notificaciones HTTP cuando ocurren eventos en tu cuenta — útil para sincronizar tu sistema interno, alertar al equipo o reaccionar automáticamente. Disponible desde el plan Starter; cap por plan en el header del response.

EventoCuándo se emite
endpoint.testDisparado manualmente desde POST /v1/webhooks/{id}/test o desde el dashboard
subscription.updatedCambio de plan, renovación, downgrade
subscription.canceledCancelación efectiva (fin del período pagado)
usage.threshold_reachedTu uso mensual cruza el umbral configurado (80% por defecto)
key.createdSe generó una API key nueva
key.revokedSe revocó una API key
key.rotatedSe rotó una API key (la previa entra en periodo de gracia)
etl.run.startedEmpezó una corrida de ETL (admin)
etl.run.succeededUna corrida de ETL terminó OK
etl.run.failedUna corrida de ETL falló
etl.run.canceledUna corrida de ETL fue cancelada
MétodoRutaPropósito
GET/v1/webhooksLista tus endpoints
POST/v1/webhooksCrea un endpoint (devuelve secret una sola vez)
GET/v1/webhooks/{id}Detalle de un endpoint
PATCH/v1/webhooks/{id}Actualiza url, events, active
DELETE/v1/webhooks/{id}Elimina el endpoint y detiene entregas
POST/v1/webhooks/{id}/rotateRota el secreto (el viejo queda válido 24 h)
POST/v1/webhooks/{id}/testDispara endpoint.test contra el endpoint
GET/v1/webhooks/{id}/deliveriesHistorial de entregas (status, intentos, response)

Todas las rutas requieren Authorization: Bearer ak_live_... con scope webhooks:manage.

Ventana de terminal
curl -X POST https://api.chequea.pe/api/v1/webhooks \
-H "Authorization: Bearer ak_live_TU_CLAVE" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-app.com/hooks/chequea",
"events": ["subscription.updated", "key.revoked", "usage.threshold_reached"],
"active": true
}'

Respuesta 201 Created:

{
"id": "wh_3b1a2f7c-2a48-4f2e-9d83-1bd5c2c9e0f1",
"url": "https://tu-app.com/hooks/chequea",
"events": ["subscription.updated", "key.revoked", "usage.threshold_reached"],
"active": true,
"secret": "whsec_5d8e3a...REVELADO_UNA_SOLA_VEZ",
"createdAt": "2026-05-20T17:00:00.000Z"
}

Cuando se dispara un evento, Chequea hace POST a tu url con:

Headers:

HeaderEjemploDescripción
content-typeapplication/jsonSiempre
user-agentChequea-Webhooks/1.0Para que puedas allowlistear
x-apiperu-eventsubscription.updatedNombre del evento
x-apiperu-deliverydlv_a1b2c3d4ID único de esta entrega (idempotencia)
x-apiperu-timestamp1747772400Unix timestamp del envío
x-apiperu-signaturet=1747772400,v1=abcd...HMAC-SHA256 (ver abajo)

Body (ejemplo subscription.updated):

{
"id": "evt_5f9a7b3c",
"type": "subscription.updated",
"createdAt": "2026-05-20T17:00:00.000Z",
"data": {
"subscriptionId": "sub_d4e5f6a7",
"plan": "pro",
"previousPlan": "starter",
"renewsAt": "2026-06-20T00:00:00.000Z"
}
}

El header x-apiperu-signature sigue el formato Stripe-like:

t=<timestamp>,v1=<signature>

Durante una rotación del secret, ambas firmas viajan por 24 horas:

t=<timestamp>,v1=<new_signature>,v0=<old_signature>

El signature es HMAC-SHA256(secret, "<timestamp>.<raw_body>") en hexadecimal lowercase. Importante: el body se firma como bytes crudos antes de cualquier parseo JSON — si tu framework re-serializa, la verificación fallará.

import { createHmac, timingSafeEqual } from "node:crypto"
function verify(req, secret, toleranceSeconds = 300) {
const sig = req.headers["x-apiperu-signature"]
const ts = Number(req.headers["x-apiperu-timestamp"])
if (!sig || !ts) return false
if (Math.abs(Date.now() / 1000 - ts) > toleranceSeconds) return false
const expected = createHmac("sha256", secret)
.update(`${ts}.${req.rawBody}`)
.digest("hex")
// Parse: "t=...,v1=...,v0=..."
const parts = Object.fromEntries(
sig.split(",").map((p) => p.split("=", 2))
)
const candidates = [parts.v1, parts.v0].filter(Boolean)
return candidates.some((c) => {
const a = Buffer.from(c, "hex")
const b = Buffer.from(expected, "hex")
return a.length === b.length && timingSafeEqual(a, b)
})
}

Recomendamos rechazar entregas con x-apiperu-timestamp más viejas que 5 minutos. Esto evita ataques de replay donde un atacante captura una entrega válida y la re-envía después.

El x-apiperu-delivery es único por intento de entrega. Persistilo en tu lado y trata duplicados como idempotentes (responder 2xx sin re-procesar).

Tu endpoint debe responder con código 2xx (preferiblemente 200, 202 o 204) en menos de 10 segundos. Cualquier otro código (o timeout, o redirect, o body > 1 MB) marca el intento como fallido y dispara reintento.

Reintentos con backoff exponencial: 0 s, 1 min, 5 min, 30 min, 2 h, 6 h, 24 h, 48 h. Después de 8 intentos fallidos la entrega se marca como failed_permanent y queda en el historial.

Casos especiales:

  • Redirects (3xx): rechazados — los webhooks deben responder directo, no apuntar a otra URL.
  • Body > 1 MB: rechazamos para no llenar tu pipe con respuestas largas; los webhooks deberían responder corto.
  • TLS inválido / DNS no resuelve: se cuenta como fallo, sigue reintento.
  • Connection refused: fallo, sigue reintento.

Las URLs de webhook se validan al crear/editar:

  • Sólo https:// en producción (http:// permitido sólo en desarrollo)
  • Bloqueamos rangos privados: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7, fe80::/10
  • Bloqueamos metadatos cloud: 169.254.169.254 (AWS/GCP/Azure)
  • DNS-pin: la IP resuelta al crear el endpoint es la que se usa al entregar (evita rebinding después)
  • Sin credenciales en URL (https://user:pass@... se rechaza)
Ventana de terminal
curl https://api.chequea.pe/api/v1/webhooks/{id}/deliveries \
-H "Authorization: Bearer ak_live_TU_CLAVE"

Devuelve hasta 100 entregas más recientes con estado, intentos, latencia y código de respuesta — útil para debugging.

PlanWebhooks máxEventosReintentosDNS-pin
Starter1todos
Pro5todos
Empresaconfigurabletodos + custom