Vivo y mantenido
Cada versión de la API queda registrada. Deriva de /v2/version + el changelog del repo.
Una librería de firma, para que no tengas que escribirla tú
Firmar cada request es lo más delicado de la integración, y los cuatro errores que costaron la primera integración de un partner eran todos el mismo: implementar el sobre canónico a mano. Unir con salto de línea en vez del byte NUL, el nonce en milisegundos, la firma con un prefijo de algoritmo delante, el idempotency-key vacío en un GET. Ahora existe una librería que los hace imposibles: cada uno tiene un test que lo reproduce y falla. Todavía no está publicada, así que esta documentación no te la ofrece — cuando lo esté, te avisamos acá. Mientras tanto el vector de prueba y el helper nextNonce() de la guía de Autenticación siguen siendo la referencia, y los dos se verifican contra esa librería en cada corrida. Lo que la librería no hace a propósito: no envuelve el ciclo de vida del pago, así que no va a romperse cuando exista el estado de devolución.
Ya puedes ver los códigos de banco, en vez de pedírnoslos
bankCode es obligatorio en los dos rieles y hasta hoy no estaba publicado en ninguna parte: la respuesta era que te pasáramos una lista por chat. Ahora hay una página, Códigos de banco, con los códigos BCV y su institución. Léela en vez de buscar en internet: varias listas que circulan dan 0138 como «Mibanco» cuando es Banco Plaza, y 0177 como «Sofitasa» cuando es BANFANB. Lo que todavía NO hacemos: no rechazamos un código que no esté en la tabla — validamos que sean 4 dígitos y, en CCE, que coincida con el prefijo de la cuenta destino. Y si no encuentras el banco de tu beneficiario, escríbenos: no podemos afirmar que la tabla sea exhaustiva. También: la página de Sandbox y la guía traen ahora las tres variables de entorno en un solo bloque para pegar, con la llave privada como ruta a un archivo y no pegada dentro del .env.
Ahora puedes saber si un pago mueve dinero de verdad
GET /v2/version trae un campo nuevo, simulado. Es true cuando ese entorno fabrica el resultado de los pagos —el pago te va a decir completed y no se mueve ni un céntimo real— y false cuando los rieles están conectados a un banco. Por qué hacía falta: rails publica lo mismo en los dos casos, así que hasta hoy no había forma de distinguirlos desde afuera: un entorno mal configurado anunciaba sus rieles, respondía completed a cada pago, y no movía nada. Qué haces con esto: míralo antes de dar por buena una prueba. Si el campo no viene, trátalo como true — prometemos de menos a propósito.
Ya puedes pagar a una cuenta bancaria, no sólo a un pago móvil
El riel "CCE" —la transferencia interbancaria— está construido. Nace cerrado: se habilita por despliegue, así que GET /v2/version deja de publicar un par fijo y el campo rails te dice qué rieles puede despachar el entorno que estás llamando. Léelo, no lo asumas — el sandbox puede tener abierto un riel que producción todavía no. Si pides uno que ese despliegue no tiene habilitado, el pago llega failed con subStatus: "rejected_by_us" y no hubo débito.
Un beneficiario CCE ahora lleva nombre, banco y documento
Con rail: "CCE" son obligatorios bankCode, docType, docNumber y el campo nuevo beneficiaryName — el nombre que viaja a la cámara de compensación, que no podemos derivar de nada y queda congelado con el resto del destino. Es un cambio incompatible y lo decimos así: aplica sólo a CCE, que todavía no está disponible de forma general y ningún despliegue de producción tiene abierto; el pago móvil no cambió. Qué haces con esto: si guardaste beneficiarios CCE antes de esta versión, no tienen beneficiaryName y no pueden pagar por ese riel. Fíltralos por beneficiaryName: null y reemplázalos — crear el nuevo primero, archivar el viejo después. No recortamos el nombre: si no cabe en 35 caracteres te devolvemos 400 al crearlo, donde lo puedes arreglar, y no al pagar, donde ya no.
El banco que declaras y la cuenta destino tienen que ser el mismo banco
Una cuenta venezolana empieza por el código BCV de su banco, así que un bankCode que no coincide con los 4 primeros dígitos de la cuenta significa que uno de los dos está mal. Antes eso salía igual hacia la cámara. Ahora te devolvemos 400 y el pago no se crea: no hay débito y tu idempotency-key queda libre. Por qué se corta antes y no después: sobre este riel no hay reverso — un rechazo tardío cuesta comisión y deja la operación en el aire.
Tu beneficiario de pago móvil ahora lleva banco y documento
Guardar un beneficiario con rail: "PM" ahora exige tres campos nuevos: bankCode (el código BCV del banco, 4 dígitos — Banco Plaza es "0138"), docType y docNumber. Por qué: el pago móvil de Venezuela identifica a quien cobra, y el riel rechaza el pago sin esos datos; no los podemos derivar, porque un prefijo como 0414 identifica a la operadora móvil, no al banco. Hasta esta versión el campo no existía en nuestro contrato, así que ningún pago móvil podía completarse. Qué haces con esto: tus beneficiarios ya guardados probablemente no tengan bankCode, y el destino es inmutable por diseño — hay que archivar el viejo y crear uno nuevo, en ese orden (crear primero da 409). Para encontrarlos, GET /v2/beneficiaries ahora devuelve bankCode: los que traen null son ésos.
Un rechazo NUESTRO ya no se reporta como si lo hubiera hecho el banco
subStatus: "declined" significa, en esta documentación, que el riel rechazó tu pago. Cuando lo bloqueamos nosotros antes de que el riel lo viera —al beneficiario le falta el banco o el documento, o falta configuración de nuestro lado— eso era falso, y te mandaba a buscar la causa donde no estaba. Esos casos ahora llegan con subStatus: "rejected_by_us". Qué cambia para ti: hay un valor nuevo bajo failed. El status sigue siendo failed y la garantía no cambia —no hubo débito—; lo que cambia es dónde buscar el problema.
Reusar una clave de idempotencia con otro banco de destino ahora es un 409
El banco y el documento del beneficiario pasan a formar parte de lo que hace única a una operación, igual que el monto y el destino. Antes, mandar la misma idempotency-key con un banco distinto te devolvía el pago original con 200 —al banco viejo— y podías creer que habías corregido. Y mandar beneficiaryId junto con campos del destino en línea, que antes se aceptaba ignorándolos en silencio, ahora es un 400 que te dice cuáles sobran.
Nueve respuestas más del riel dejaron de reportarse como failed
Misma corrección que en v0.22.0, ahora sobre nueve códigos más. failed significa, en esta documentación, «garantizado que no hubo débito, puedes reemitir con clave nueva» — y una clave nueva desactiva la de-duplicación del riel, así que darlo mal termina en una segunda transferencia real. Revisamos código por código qué dice el riel de cada uno: tres son duplicados (111, 112, 150) y no afirman nada sobre el destino de la operación original; dos son vencimientos de espera (AB01, AB05), que hablan de un reloj y no del dinero; los otros cuatro (A001, A002, A004, 0012) llevaban un comentario nuestro que decía que se rechazan antes de tocar el core, y el riel no dice eso en ningún lado. Qué cambia para ti: si tu sistema ramifica sobre failed para reemitir, esos casos ahora llegan como unknown — no reemitas, consulta y espera la resolución.
El identificador que ve el banco lo generamos nosotros
Tu idempotency-key sigue siendo tuya y sigue funcionando igual. Lo que cambió es qué viaja al riel: antes iba tu clave tal cual, y el riel se queda con sus últimos 8 caracteres — así que dos claves distintas que terminan igual eran el mismo pago para él. Ahora acuñamos un identificador propio de 8, único por construcción.
Una transferencia a otro banco ya no se marca completada mientras va en camino
El riel responde «transacción exitosa / enviada a la cámara», y ese «enviada» es literal: si el beneficiario tiene cuenta en otro banco, la operación sigue en vuelo. Ahora esos pagos quedan en pending hasta que la liquidación se confirma, en vez de reportarse como completed antes de tiempo.
Tres respuestas del riel dejaron de reportarse como failed
Tres códigos —fuera de franja horaria, sistema fuera de línea, y «no se puede procesar»— estaban clasificados como «probadamente no se debitó», y eso te llegaba como failed. Ninguno de los tres lo prueba. Como nuestra guía te dice que ante un failed puedes reemitir con clave nueva, y una clave nueva desactiva la de-duplicación del riel, el peor caso era una segunda transferencia real. Ahora llegan como unknown, que es la verdad: no sabemos todavía. Qué cambia para ti: vas a ver menos failed y más unknown en esos escenarios — no reemitas, espera la resolución.
«No pude comprobarlo» dejó de tratarse como «no pasó»
Cuando un pago queda en duda lo consultamos al riel. Si esa consulta no concluye, antes escalábamos a intervención manual en el primer intento. Ahora se re-consulta dentro de una ventana antes de escalar: menos pagos detenidos esperando a una persona.
pending dejó de significar «no sé»
Un riel asíncrono que confirma tener el pago no es lo mismo que un riel que no contesta. Antes los metíamos en el mismo balde y levantábamos una alerta crítica; ahora pending es un estado de tránsito normal y se sigue consultando.
El servicio se niega a arrancar si la base retrocedió en el tiempo
Endurecimiento interno de la protección anti-replay. No cambia nada de tu integración; se publica porque sostiene la garantía de que una petición firmada tuya no pueda reutilizarse.
Corregida la recomendación de nonce en la guía de autenticación
Decía que «un timestamp en milisegundos suele bastar». Medido: con 8 peticiones en paralelo pasa exactamente una, porque en el mismo milisegundo todos los nonces son iguales. La guía ahora recomienda un nonce con aleatoriedad y trae un helper ejecutable.
Las lecturas firmadas ya no te obligan a serializar
Ventana anti-replay deslizante (RFC 4303) en las 7 rutas GET. Antes, dos lecturas en paralelo desde una misma llave se rechazaban entre sí. Medido: la aceptación en paralelo pasó de 31 % a 100 % y la latencia p99 de 111 ms a 21 ms. Las escrituras conservan la marca estricta.
La doc de autenticación nunca mencionaba el header x-nonce
Y además afirmaba que no era un header. Corregido en la guía y en el contrato.
La página de Conciliación prometía una automatización que no existe
Decía que emparejábamos el crédito de una devolución con el pago original y lo reflejábamos solo. No es así: un crédito sin fondeo registrado queda como excepción y lo trabaja una persona. La página ahora dice lo que el sistema hace.
Los webhooks quedan garantizados de punta a punta
El drenador del canal asíncrono cierra el circuito: todo cambio de estado de un pago genera su evento en la misma transacción que el cambio, y el envío se reintenta con respaldo propio. Ningún cambio de estado puede quedarse sin notificar.
Guard de arranque contra una base sin migrar
Interno. El servicio se niega a arrancar si el esquema no está al día, en vez de servir peticiones contra una base incompleta.
Outbox de eventos: ningún cambio de estado sin su evento
La fila del evento se escribe en la misma transacción que el cambio de estado del pago. Si una de las dos falla, no pasa ninguna.
Un webhook caído dejó de ser indistinguible de uno que recibió todo
Antes descartábamos la respuesta HTTP de tu endpoint, así que un 500 tuyo no dejaba rastro — mientras la doc prometía reintentos. Ahora la respuesta se registra y el reintento es real.
Los 27 parámetros de header salían required: false en el contrato
Reportado por un integrador. La prosa los llamaba obligatorios pero el contrato decía lo contrario, así que un generador de clientes hacía opcionales las credenciales. Corregido: si generaste un cliente antes de esta versión, vuelve a generarlo.
Gate de fidelidad doc↔contrato, bloqueante
Interno. Ninguna afirmación de esta documentación puede contradecir al contrato OpenAPI sin romper el build. Es la razón por la que lo que lees acá coincide con lo que responde la API.
Gate de despliegue del API
Interno. Cierra el hueco de que un arreglo se mergee y el ambiente siga sirviendo la versión vieja: ahora el despliegue se verifica contra la huella del build.
Versión + discovery — GET /v2/version
Endpoint público que un consumidor llama antes de firmar: release vivo, rieles soportados y enlaces al contrato.
Conciliación automática (recon)
Cuadre continuo del saldo del banco contra el ledger: caza débitos huérfanos, dobles-débitos y pagos sin fondeo.
Identidad de tu llave — GET /v2/me
Endpoint firmado y read-only: qué entidad eres, qué ambiente, tu webhook y tus límites. Sin filtrar ningún secreto.
Beneficiarios reutilizables
Guarda un beneficiario una vez y págale por beneficiaryId. Destino congelado tras crearlo (money-safety).
Contrato OpenAPI público
/v2/openapi.json derivado del schema real: la referencia nunca discrepa de lo desplegado.
Firma de webhooks salientes
Webhooks firmados con RSA asimétrica + JWKS: verificas el origen sin secreto compartido.