neo-pays

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 200 en 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 amount y currency donde la API lleva amount_minor y asset_id, y merchant_reference donde ella lleva client_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.stringify de 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.