Webhooks
La fuente de verdad de sus integraciones: cada cambio de estado se le envía, firmado, con reenvío automático.
Crear un endpoint
POST /v1/webhooks
{
"url": "https://susitio.com/webhook/neopays",
"description": "producción",
"subscribed_events": ["payment.success", "refund.success"]
}
La respuesta lleva el secreto de firma. subscribed_events vacío = todos los eventos; un evento
fuera del catálogo se rechaza en lugar de guardarse, porque esperaría una entrega que nada emite.
Esta llamada asegura, no siempre crea. Una URL ya registrada vuelve con
200en lugar de duplicarse — un script de aprovisionamiento ejecutado dos veces deja UN endpoint, no dos recibiendo cada evento por duplicado. El secreto se devuelve en ambos casos, porque la razón legítima para volver a llamar es haberlo perdido.
GET /v1/webhooks — sus endpoints y sus contadores
GET /v1/webhooks/{id} — un endpoint
PATCH /v1/webhooks/{id} — modificación parcial
DELETE /v1/webhooks/{id} — supresión definitiva
GET /v1/webhooks/{id}/secret — releer el secreto
POST /v1/webhooks/test — entrega de prueba a todos sus endpoints activos
El PATCH sólo cambia lo que usted envía: omitir is_active no desactiva el endpoint. Para detener
las entregas sin perder el endpoint ni su secreto, envíe is_active: false en lugar de suprimirlo.
Diez endpoints por cuenta como máximo. El panel hace todo esto también, y sólo él sabe reenviar una entrega fallida y rotar un secreto.
Los eventos
| Evento | Cuándo |
|---|---|
payment.initiated |
Cobro creado |
payment.pending |
A la espera de la confirmación del pagador |
payment.success |
Cobrado — el evento que cuenta |
payment.failed |
Fallo |
payout.success / payout.failed / payout.cancelled |
Ciclo de vida de un pago |
payout.refunded |
Pago devuelto por el rail después |
refund.success / refund.failed |
Ciclo de vida de un reembolso |
El sobre es siempre el mismo:
{
"event": "payment.success",
"created": "2026-08-24T12:00:00Z",
"data": {
"id": "op_01J8Z9K2QW",
"reference": "NP-...",
"merchant_reference": "pedido-1042",
"amount": "5000",
"currency": "XOF",
"status": "success"
}
}
El cuerpo del webhook no tiene la misma forma que la respuesta de la API. Lleva
amountycurrencydonde la API llevaamount_minoryasset_id, ymerchant_referencedonde ella llevaclient_ref. Es un contrato distinto, escrito antes que el otro y sin cambios desde entonces: no suponga que un objeto de uno se decodifica como un objeto del otro.
Verificar la firma
Cada entrega lleva la cabecera:
X-NeoPays-Signature: t=<marca de tiempo unix>,v1=<hmac>
donde hmac = HMAC-SHA256(secreto, "<t>.<cuerpo en bruto>"), en hexadecimal. Verifique en tres
tiempos:
const crypto = require('node:crypto')
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map(kv => kv.split('=')))
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSec) return false
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`).digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
El cuerpo EN BRUTO, no el vuelto a serializar. Calcule el HMAC sobre los bytes recibidos, antes de cualquier análisis JSON. Un
JSON.stringifyde vuelta puede reordenar las claves e invalidar la firma.
Durante una rotación de secreto la cabecera lleva DOS entradas
v1=, el secreto actual primero. Verifíquelas todas, no sólo la primera: el día en que usted cambie su configuración al secreto nuevo antes de que termine el cambio del lado de la plataforma, la primera firma es la del secreto antiguo, y un verificador que se detenga en la primera rechazará entonces el 100 % de las entregas.
Reenvío
Responda 2xx en menos de unos segundos (acuse recibo, procese después). Cualquier otra respuesta
dispara la escalera de reintentos: 5 intentos en total, tras 60 s, 5 min, 30 min y 2 h.
Un endpoint que falla 20 entregas consecutivas queda suspendido una hora (las entregas se reanudan solas); un solo éxito — incluida una entrega de prueba desde el panel — lo rehabilita de inmediato.
Idempotencia del lado del receptor
Las entregas son at-least-once: el mismo evento puede llegar dos veces. Deduplique por la pareja
(event, data.id) o registre los identificadores de eventos ya procesados.