Webhooks (referencia)
Un webhook es la manera en que Alibanca te avisa —sin que tengas que preguntar— cuando cambia el estado de un pago. En vez de que tu sistema esté tocando la puerta cada rato ("¿ya se movió? ¿ya se movió?"), tú registras una URL tuya y nosotros te tocamos la puerta a ti: cada vez que un pago pasa a completed, failed o unknown, te enviamos un evento firmado a esa dirección. Este recurso agrupa los cuatro endpoints con los que configuras, pruebas y verificas ese canal de avisos.
Qué es un webhook y por qué te conviene
Piensa en la diferencia entre revisar el buzón cada cinco minutos y que te llegue una notificación al teléfono. Lo primero es polling (consultar tú mismo con GET /v2/payments/:id → una y otra vez). Lo segundo es un webhook: nosotros iniciamos una petición HTTP POST hacia una URL que tú controlas, con el evento adentro, en el momento en que el estado cambia. Es empujar (push) en lugar de halar (pull).
Un webhook es simplemente un endpoint HTTP tuyo —una ruta en tu servidor, por ejemplo https://tuservidor.com/alibanca/webhooks— preparada para recibir peticiones POST con un cuerpo JSON. Alibanca las envía; tú las lees, verificas que de verdad vienen de nosotros y actualizas tu base de datos. Eso es todo el contrato.
GET /v2/payments/:id. Trata al webhook como "llegó rápido la buena noticia" y al poll como "el respaldo que nunca miente".Cuándo se dispara un evento
Alibanca te envía un evento cuando cambia el estado (state) de un pago. Los estados posibles son tres, y cada uno significa algo muy distinto para tu integración:
completed — el dinero se movió; el pago quedó firme. failed — el pago no ocurrió y hay garantía de que no hubo débito: es seguro reintentar la operación lógica con una idempotency-key nueva. unknown — el banco no respondió a tiempo (timeout o caída) y todavía no sabemos si el dinero se movió o no; la conciliación lo resolverá. Ante un unknown nunca creas un pago nuevo: se resuelve solo, por cuadre de saldo.
unknown NO es una orden de reintentar. Es lo contrario: es el aviso de "quédate quieto, lo estamos reconciliando". Si tu handler de webhooks reintenta automáticamente al ver unknown, te expones a un doble-pago. El monto mágico que termina en .03 en sandbox existe precisamente para que pruebes que tu integración NO re-emite en ese caso.Los cuatro endpoints del recurso
Todo lo relacionado con webhooks vive bajo /v2/webhook. Con estos cuatro endpoints ves tu configuración actual, cambias la URL de destino, disparas un evento de prueba y descargas las llaves públicas para verificar las firmas.
GET /v2/version, GET /v2/openapi.json y GET /health, todo en /v2 exige tu x-api-key y la firma x-signature del envelope canónico. Es decir: para configurar tu webhook firmas la petición igual que firmarías un pago. Cómo se arma esa firma está en Autenticación y firma →.GET /v2/webhook — ver la configuración actual
Devuelve la configuración de webhook asociada a tu llave: a qué URL estamos enviando los eventos y si hay un cambio pendiente de aplicarse. No recibe parámetros en el cuerpo. Úsalo para confirmar, antes de empezar a operar, que registraste la dirección correcta.
Respuesta
| Campo | Tipo | Descripción |
|---|---|---|
| url | string · null | La URL de tu endpoint a la que enviamos los eventos. Es null si todavía no has configurado ninguna (en cuyo caso solo recibes estados por poll). |
| pendingChange | object · null | Un cambio de configuración en tránsito que aún no está firme —por ejemplo, una URL nueva enviada con PUT que todavía no termina de aplicarse. Es null cuando no hay nada pendiente y lo que ves en url es la configuración activa. Sirve para detectar el estado intermedio "pedí el cambio pero aún no rige". |
curl https://alibanca-api-production.up.railway.app/v2/webhook \ -H "x-api-key: sk_test_tu_llave" \ -H "x-signature: <firma RSA-SHA256 del envelope>"
{ "status": "success", "traceId": "trc_7f3a91c2e0", "payload": { "url": "https://tuservidor.com/alibanca/webhooks", "pendingChange": null } }
La misma información aparece resumida dentro de GET /v2/me →, bajo el campo webhook: { url, pendingChange }. Si solo quieres una vista rápida de la identidad de tu llave junto con tu webhook, /v2/me te la da de un solo golpe.
PUT /v2/webhook — configurar o actualizar la URL
Establece —o reemplaza— la URL a la que Alibanca enviará los eventos. Es una operación idempotente por diseño (PUT significa "deja el recurso en este estado"): si envías dos veces la misma URL, el resultado final es el mismo. Aun así, como toda escritura autenticada, viaja con su idempotency-key para que un reintento por corte de red no genere efectos raros.
Cuerpo de la petición
| Campo | Tipo | Descripción |
|---|---|---|
| urlreq | string | La dirección HTTPS de tu endpoint receptor. Debe ser una URL pública alcanzable desde internet y bajo tu control. Es donde llegarán los POST con cada evento de cambio de estado. |
/alibanca/webhooks) que solo haga esta tarea: verificar la firma y encolar el evento. No la mezcles con otras rutas de tu aplicación.# El cuerpo crudo (raw body) que firmas debe ser EXACTAMENTE # el mismo que envías, byte por byte. curl -X PUT https://alibanca-api-production.up.railway.app/v2/webhook \ -H "x-api-key: sk_test_tu_llave" \ -H "x-signature: <firma RSA-SHA256 del envelope>" \ -H "idempotency-key: wh-cfg-2026-07-22-01" \ -H "Content-Type: application/json" \ -d '{"url":"https://tuservidor.com/alibanca/webhooks"}'
{ "status": "success", "traceId": "trc_a20b4d81f9", "payload": { "url": "https://tuservidor.com/alibanca/webhooks", "pendingChange": null } }
Este es el mismo flujo de firma que usas para emitir un pago. En Node, la parte importante es construir el envelope canónico —la concatenación determinística de [método, ruta, api-key, idempotency-key, cuerpo crudo, nonce] en ese orden— y firmarlo con tu llave privada. El nonce debe ser mayor que el de tu petición anterior (marca de agua creciente) para que no pueda reproducirse.
// buildEnvelope() y sign() implementan el envelope canónico exacto // descrito en la guía de Autenticación y firma. La forma canónica // (orden y separadores) es fija; no la improvises. const path = "/v2/webhook"; const apiKey = "sk_test_tu_llave"; const idempotencyKey = "wh-cfg-2026-07-22-01"; const nonce = nextNonce(apiKey); // estrictamente creciente por llave const rawBody = JSON.stringify({ url: "https://tuservidor.com/alibanca/webhooks" }); const envelope = buildEnvelope({ method: "PUT", path, apiKey, idempotencyKey, rawBody, nonce }); const signature = sign(envelope, tuLlavePrivada); // RSA-SHA256 const res = await fetch("https://alibanca-api-production.up.railway.app" + path, { method: "PUT", headers: { "x-api-key": apiKey, "x-signature": signature, "idempotency-key": idempotencyKey, "Content-Type": "application/json" }, body: rawBody });
pendingChange. Cuando el cambio de URL requiere un paso intermedio antes de quedar firme, PUT te lo refleja: la url activa sigue siendo la anterior y el objeto pendingChange describe la nueva en tránsito. Consulta GET /v2/webhook hasta ver pendingChange: null para confirmar que la dirección nueva ya rige. Si el cambio aplica de inmediato, verás la URL nueva y pendingChange: null desde la propia respuesta del PUT.POST /v2/webhook/test — disparar un evento de prueba
Envía un evento de prueba a la URL que tienes configurada, para que valides toda la cadena de recepción sin esperar a que ocurra un pago real: que tu servidor está accesible, que estás leyendo el cuerpo, y —lo más importante— que verificas la firma correctamente. Es la forma recomendada de estrenar (o de depurar) tu handler antes de mover un solo céntimo.
No necesita parámetros de destino: el evento va a la url que ya registraste con PUT /v2/webhook. Si no tienes URL configurada, configúrala primero. La respuesta del API te confirma que despachamos el evento; el resultado real —que tu servidor lo recibió y respondió 2xx— lo observas en tus propios logs.
curl -X POST https://alibanca-api-production.up.railway.app/v2/webhook/test \ -H "x-api-key: sk_test_tu_llave" \ -H "x-signature: <firma RSA-SHA256 del envelope>" \ -H "idempotency-key: wh-test-2026-07-22-01"
{ "status": "success", "traceId": "trc_c91e07ab34", "payload": { "delivered": true, "url": "https://tuservidor.com/alibanca/webhooks" } }
Lo que tu servidor recibe es un POST con un cuerpo JSON firmado, con la misma forma que un evento real de cambio de estado de un pago. Tu handler debe: (1) leer el cuerpo crudo, (2) verificar la firma con el JWKS, (3) responder rápido con 200, y (4) procesar el evento de forma idempotente. Un evento de prueba está marcado como tal para que no lo confundas con dinero de verdad —trátalo exactamente igual en cuanto a verificación, pero no lo apliques a un saldo real.
https://alibanca-api-production.up.railway.app) todo corre contra el proveedor simulado: nada mueve un centavo real. Configura tu URL con PUT /v2/webhook, dispara POST /v2/webhook/test y observa el POST entrante en tus logs. Si tu verificación de firma falla con el evento de prueba, fallará también en producción: arréglalo aquí, gratis, antes de emitir tu primer pago.GET /v2/webhook/keys — el JWKS de verificación
Devuelve un JWKS (JSON Web Key Set): el conjunto de llaves públicas de Alibanca con las que verificas que cada webhook entrante de verdad lo firmamos nosotros. Los webhooks van firmados con RSA asimétrica, así que la verificación se hace sin secreto compartido: tú solo necesitas nuestra llave pública, y nosotros guardamos la privada. Es el mismo principio que un sello con firma que cualquiera puede reconocer, pero que solo el dueño del sello puede estampar.
¿Por qué un conjunto de llaves y no una sola? Porque las llaves rotan. Para cambiar de llave sin cortar tu servicio, durante un tiempo publicamos la vieja y la nueva a la vez; cada evento firmado incluye un identificador de llave (kid) que te dice cuál del conjunto usar. Por eso nunca fijes una sola llave a mano: consulta este endpoint, cachéalo por un rato razonable y selecciona la llave por su kid.
curl https://alibanca-api-production.up.railway.app/v2/webhook/keys \ -H "x-api-key: sk_test_tu_llave" \ -H "x-signature: <firma RSA-SHA256 del envelope>"
{ "status": "success", "traceId": "trc_5b8e12f0aa", "payload": { "keys": [ { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "whk_2026_07", "n": "0vx7agoebGcQSuuPiLJXZ...<módulo RSA en base64url>...w0WKfQ", "e": "AQAB" }, { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "whk_2026_05", "n": "qL8R4QICLLo...<llave anterior, aún válida durante la rotación>...tVfY9Q", "e": "AQAB" } ] } }
Qué significa cada campo del JWKS
| Campo | Tipo | Descripción |
|---|---|---|
| kty | string | Tipo de llave. Siempre RSA, coherente con la firma RSA-SHA256. |
| use | string | Uso previsto de la llave: sig (signature), es decir, para verificar firmas —no para cifrar. |
| alg | string | Algoritmo: RS256, que es el nombre estándar de RSA con SHA-256. |
| kid | string | Key ID. El identificador que trae cada webhook para decirte cuál llave del conjunto usar. Es la pieza que hace posible rotar sin romper. |
| n | string | El módulo RSA en base64url. Junto con e reconstruye la llave pública que tu librería de criptografía necesita. |
| e | string | El exponente público en base64url. Casi siempre AQAB (el número 65537). |
Cómo verificar la firma de un webhook entrante
Verificar no es opcional: es lo que distingue "un aviso legítimo de Alibanca" de "cualquiera que descubrió tu URL y te manda POST falsos". Como usamos firma asimétrica, no compartes ningún secreto; solo compruebas que la firma cuadra con nuestra llave pública. El flujo en tu handler es este:
Lee el cuerpo CRUDO, sin re-serializar
Guarda el body exactamente como llegó, byte por byte. Si tu framework parsea el JSON y luego lo vuelves a convertir a texto, cambias espacios u orden de claves y la firma ya no cuadra. Verifica sobre el raw body, no sobre el objeto reconstruido.
Toma el kid del evento y busca su llave
El webhook indica con qué llave lo firmamos. Busca en tu JWKS cacheado la entrada cuyo kid coincida. Si no la encuentras, refresca desde GET /v2/webhook/keys (probablemente rotamos) y vuelve a intentar.
Verifica la firma RSA-SHA256
Con la llave pública (n + e) comprueba la firma contra el cuerpo crudo usando RS256. Si no valida, rechaza el evento con un error y no lo proceses. No hay "casi válido": o la firma cuadra o no es nuestra.
Responde 2xx rápido y procesa idempotente
Contesta 200 apenas verifiques, para que no reintentemos por timeout. Haz el trabajo pesado después, de forma idempotente: si el mismo evento llega dos veces, tu sistema debe llegar al mismo estado sin duplicar nada.
Buenas prácticas y errores comunes
Idempotencia en tu receptor
Los webhooks pueden llegar más de una vez —un reintento nuestro por un timeout tuyo, o una condición de red, provocan entregas repetidas. Diseña tu handler para que reprocesar el mismo evento no cause daño: identifica cada evento por el pago y su estado, y aplica el cambio solo si aún no lo aplicaste. Si tu lógica "suma" o "re-emite" al recibir, un evento repetido te va a doler.
El webhook es señal, el poll es verdad
Si nunca llega un evento —tu servidor estuvo caído, un despliegue tumbó la ruta— tu integración no puede quedar ciega. Mantén siempre un poll de respaldo con GET /v2/payments/:id → o un barrido con GET /v2/payments → para los pagos que sigas esperando. El webhook acelera; el poll garantiza. Un pago en unknown no lo resuelve ni el webhook ni el poll: lo resuelve la conciliación →, y ahí tu trabajo es no reintentar.
Cachea el JWKS, pero refréscalo ante un kid desconocido
Pedir el JWKS en cada evento es innecesario y lento; cachearlo para siempre te deja fuera cuando rotamos. El punto medio: cachea por un rato razonable y, si te llega un evento con un kid que no tienes, refresca desde GET /v2/webhook/keys y reintenta la verificación. Así absorbes las rotaciones sin intervención manual.
No mezcles sandbox y producción
Las llaves de sandbox (sk_test_…) y su JWKS son distintas de las de producción (sk_live_…). Configura una URL de webhook por entorno y verifica con el JWKS del entorno correcto. Un evento de sandbox verificado contra llaves de producción —o al revés— fallará, y con razón.
PUT /v2/webhook; verificar quién eres y a dónde apunta tu webhook es GET /v2/me; validar de punta a punta es POST /v2/webhook/test; y confiar en lo que llega es cuestión de GET /v2/webhook/keys. Con esos cuatro tienes el canal de avisos completo y auditable.