Webhooks de Nomba
Un webhook es la forma en que Nomba le dice a tu servidor que el dinero realmente se movió. Cualquier persona en Internet puede hacer POST a tu URL de webhook, por lo que el manejador debe probar que cada solicitud proviene de Nomba antes de marcar un pedido como pagado o reembolsar a alguien. También debe lidiar con los reintentos de Nomba del mismo evento.
Usa el verificador del SDK oficial; no implementes manualmente el HMAC. La firma de Nomba cubre campos específicos unidos con :, y los SDKs también bloquean un truco de replay que una implementación ingenua pasaría por alto.
| Lenguaje | Verificar |
|---|---|
Node.js / TS (@nombaoss/nomba-node) | verifyWebhook(rawBody, headers, signingKey) |
Python (nomba) | from nomba.webhooks import verify_signature → verify_signature(raw_body, headers, signing_key) |
PHP (nombaoss/nomba-php) | Nomba\Webhook::verify($rawBody, $headers, $signingKey) |
Las importaciones exactas y los tipos de argumentos para cada lenguaje están en references/sdk-setup.md. Cada uno devuelve el evento analizado o lanza una excepción (WebhookVerificationError / WebhookVerificationException). Ninguno necesita credenciales de API, solo la clave de firma del webhook desde el panel de Nomba (Desarrollador → Configuración de Webhook). Léela desde una variable de entorno como NOMBA_WEBHOOK_SIGNING_KEY. Es un secreto diferente al secreto del cliente de la API.
El manejador, paso a paso
- Obtén el cuerpo sin procesar. Verifica los bytes exactos que Nomba envió. Si un analizador JSON se ejecuta primero y vuelves a serializar el objeto, la firma puede dejar de coincidir, o peor, terminas verificando algo diferente de lo que ejecutas. Cada framework tiene una forma diferente de obtener el cuerpo sin procesar; consulta references/frameworks.md para la correcta, y excluye la ruta de la protección CSRF cuando el framework la tenga.
- Verifica, luego analiza. Usa el evento que devuelve el verificador. En caso de fallo, responde con
401(o400) y detente. No registres el cuerpo completo ni los encabezados de las solicitudes rechazadas a nivel de información. - Desduplica por
requestId. Nomba reintenta las entregas, por lo que el mismo evento puede llegar más de una vez. Registra cadarequestIdprocesado con una restricción única y omite los que ya hayas visto. Devuelve200también para los duplicados, o Nomba seguirá reintentando. - Reconoce rápido. Devuelve
200rápidamente y realiza el trabajo lento (correos electrónicos, cumplimiento, actualizaciones de contabilidad) en un trabajo en segundo plano claveado porrequestId. Los manejadores lentos causan tiempos de espera y redeliveries. - Actúa según el evento utilizando la tabla a continuación. Empareja el evento con tu propio registro mediante la referencia, y verifica que el monto y la moneda coincidan con lo que esperabas antes de cumplir.
Qué significa cada evento
event_type | Coincide con | Hacer |
|---|---|---|
payment_success | checkout: data.order.orderReference; cuenta virtual / transferencia entrante: data.transaction.aliasAccountReference o transactionId | Marcar como pagado después de verificar data.transaction.transactionAmount (y moneda) contra tu pedido. Para checkouts tokenizados, almacena data.tokenizedCardData.tokenKey (si está presente y no es "N/A") con el cliente, solo del lado del servidor. |
payment_failed | igual que arriba | Marcar el intento como fallido; permite que el cliente reintente. No se cobró nada. |
payment_reversal | igual que arriba | El dinero que recibiste fue revertido. Deshaz el cumplimiento o señálalo para revisión. |
payout_success | data.transaction.merchantTxRef | Marcar la transferencia o el pago de factura como entregado. |
payout_failed | data.transaction.merchantTxRef | No reembolses todavía. Nomba puede reintentar a través de otra ruta. Márcalo como fallido y espera. |
payout_refund | data.transaction.merchantTxRef | El dinero ha vuelto a tu billetera de Nomba. Ahora es seguro reembolsar o acreditar a tu cliente y marcar el pago como fallido. |
Reembolsar en payout_failed (o en un tiempo de espera) en lugar de payout_refund es la forma clásica en que los comerciantes pagan dos veces a los clientes. Para cualquier evento que no reconozcas, regístralo y devuelve 200.
Confirma las decisiones económicas con la API. La firma de Nomba cubre solo event_type, requestId, data.merchant.userId, data.merchant.walletId, data.transaction.transactionId, type, time, responseCode y la marca de tiempo. El monto, orderReference, merchantTxRef y el tokenKey de la tarjeta no están firmados. Por lo tanto, antes de cumplir, reembolsar o almacenar un token de tarjeta, vuelve a consultar con el SDK y actúa según la respuesta de la API: transactions.get({ orderReference }) para pagos de checkout (también checkout.getTransaction en producción) y transactions.get({ merchantTxRef }) para pagos. Desduplicar en el requestId firmado detiene una copia modificada de un evento que ya has procesado, y la re-consulta cubre el resto.
Protección contra replay
Desduplicar por requestId es la defensa principal: una entrega repetida o reintentada de un evento que ya has procesado no cambia nada.
Los verificadores también pueden rechazar marcas de tiempo antiguas (toleranceSeconds / tolerance_seconds / el cuarto argumento en PHP), pero deja esto desactivado por defecto. Nomba documenta nomba-timestamp como la hora en que se envió el payload, sin decir si los reintentos automáticos obtienen una nueva. Si mantienen la original, una tolerancia más corta que la ventana de reintento de Nomba rechazaría reintentos legítimos después de una interrupción, y los pedidos pagados nunca se confirmarían. Habilítalo solo después de confirmar en el sandbox que las redeliveries llevan una nueva marca de tiempo, y luego mantenlo generoso (por ejemplo, una hora) con los relojes del servidor sincronizados.
Ejemplo (Node.js + Express)
import express from "express";
import { verifyWebhook, WebhookEventType } from "@nombaoss/nomba-node";
const app = express();
// Cuerpo sin procesar solo para esta ruta; regístralo antes de cualquier express.json() global.
app.post("/webhooks/nomba", express.raw({ type: "*/*" }), async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.headers, process.env.NOMBA_WEBHOOK_SIGNING_KEY!);
} catch {
return res.sendStatus(401);
}
// Restricción única en request_id; devuelve false si ya lo hemos visto.
const isNew = await webhookEvents.recordOnce(event.requestId, event.event_type);
if (isNew) await jobs.enqueue("nomba-webhook", event); // procesar en segundo plano
res.sendStatus(200);
});
Python (Flask/FastAPI/Django), PHP (Laravel/plano) y otros frameworks de Node están en references/frameworks.md.
Pruebas
- Prueba unitaria del manejador con el vector de prueba documentado del SDK: cuerpo de
webhook-fixture.jsondel SDK, claveSampleSignatureKey123, encabezadonomba-timestamp: 2025-09-29T10:51:44Z,nomba-signature: q30yFZDyykoymBf9+mt1wz5UAuLk57raPGPHlGzIv00=. Afirma que una clave incorrecta, un cuerpo manipulado y encabezados faltantes son rechazados, y que unrequestIdduplicado se procesa una vez. - De extremo a extremo: establece una URL de webhook de prueba y una clave de firma en el panel, expón tu servidor local con un túnel y realiza pagos en sandbox. El panel puede reenviar o depurar entregas (https://developer.nomba.com/docs/api-basics/troubleshoot-webhooks).
Lista de verificación
- El cuerpo sin procesar se verifica con el SDK, antes de cualquier análisis o lógica de negocio.
- La clave de firma proviene del entorno, no del código.
- Las firmas incorrectas reciben 401/400; no ocurre nada más.
-
requestIdse desduplica con una restricción única; los duplicados aún reciben 200. - El trabajo pesado se ejecuta en segundo plano; el manejador devuelve 200 rápidamente.
- El monto y la moneda se verifican contra el pedido antes de cumplir.
- Los reembolsos ocurren solo en
payout_refund(o una re-consulta que devuelvaREFUND). - La ruta está excluida de la protección CSRF y usa HTTPS.