Saltearse al contenido

Changelog

Acá listamos cambios que afectan a integradores: endpoints nuevos, campos agregados/cambiados, deprecaciones. Mejoras del dashboard o internas no entran.

2026-10

  • Botón “Volver al comercio” en las pantallas finales del checkout. Con el checkout_url abierto como página completa y un return_url que es un deep link (fixapp://...), Chrome en Android no abre tu app desde la redirección automática: muestra un cartel que se va solo a los segundos, y el comprador quedaba en la página sin forma de volver. Ahora las pantallas finales (aprobado, rechazado, cancelado, vencido; de pago y de catastro) muestran un botón que lleva al mismo return_url con el mismo status. La redirección automática sigue igual. Tu app puede recibir el mismo deep link dos veces: procesalo de forma idempotente. Ver integración en apps móviles.
  • La página del checkout se entera antes de una cancelación. Si cancelás un pago con el formulario abierto, la página lo saca en hasta 5 segundos (antes, 15). Y si el comprador abre el checkout_url de un pago ya cancelado o vencido, ya no ve el formulario: antes lo veía, y podía pagar, hasta la primera consulta. Ver Cancelar.
  • Cancelar un pago que se aprobaba en el mismo instante ya no lo deja cancelado. Si POST /v1/payments/{id}/cancel llegaba justo cuando el proveedor aprobaba el pago, podía quedar cancelado con la plata cobrada, y te llegaban payment.confirmed y después payment.cancelled. Ahora el cancel responde 422 y el pago queda aprobado.
  • return_url y cancel_url no aceptan esquemas que ejecutan código. javascript:, data:, vbscript:, file:, blob: y about: responden 422 en POST /v1/payments, POST /v1/cards y POST /v1/cards/{id}/charge. Los deep links (fixapp://) y las URLs http/https siguen igual.
  • Los pagos de Dinelco ya no vencen antes de su plazo. Vencíamos el pago cuando vencía el token de Dinelco para abrir el formulario (unos 5 minutos en live), en vez de en el plazo que pediste con expires_in: un pago con 15 minutos te mandaba payment.expired a los 5, y si el comprador pagaba después te llegaba un payment.confirmed marcado como recuperado. Ahora el pago vence en su plazo (expires_at). checkout.expires_at sigue siendo hasta cuándo se puede abrir el formulario. Ver dos vencimientos distintos.
  • La página del checkout_url saca el formulario al vencer o al cancelar el pago. Un formulario de tarjeta ya abierto aceptaba pagos sin límite de tiempo. Ahora, al llegar el plazo del pago, la página saca el formulario y avisa con postMessage ingalca.checkout.expired, o redirige a tu return_url con status=expired si está abierta como página completa. Si cancelás el pago con POST /v1/payments/{id}/cancel, en hasta 15 segundos pasa lo mismo con ingalca.checkout.cancelled. Si escuchás los postMessage o la vuelta por return_url, sumá el caso expired. Ver Cancelar.
  • Nuevo campo card: con qué tarjeta se pagó. En la API y en los webhooks payment.*, con el tipo (credit o debit), la marca y el número enmascarado: { "type": "debit", "brand": "visa", "masked_number": "445653******1096" }. Viene también en los pagos rechazados, y es null en transferencias. Dinelco informa los tres datos. Bancard informa solo el tipo, y credit puede ser un débito que no procesa Bancard. Es un campo agregado, no cambia nada existente. Ver tarjeta usada.
  • Nuevo campo provider_approved_at: cuándo aprobó el proveedor. confirmed_at es cuándo confirmamos nosotros el pago; si un aviso del proveedor se perdió, nos podemos enterar minutos después de que el comprador pagó. provider_approved_at es la hora de la aprobación según el proveedor, para que sepas si el comprador pagó a tiempo aunque te enteres tarde. Dinelco lo informa; con Bancard viene null. confirmed_at y confirmed_after_expiry no cambian. Si reservás stock o cupo, mirá qué hacer según cuándo liberás la reserva.
  • El cobro con tarjeta guardada puede volver pending. POST /v1/cards/{id}/charge casi siempre devuelve confirmed o failed, pero Dinelco a veces tarda más de 30 segundos. Antes, en ese caso respondíamos un error 502 sin payment_id y el pago quedaba abierto para siempre, sin webhook. Ahora respondemos 201 con el pago en pending y en uno o dos minutos le preguntamos a Dinelco cómo terminó: te llega payment.confirmed o payment.failed. Si en 10 minutos Dinelco no informa nada, el pago pasa a failed. No reintentes con otra reference: si el primero se aprobó, cobrarías dos veces. Si el cobro seguro no se hizo (no pudimos conectar, o Dinelco rechazó el pedido), seguís recibiendo el error de siempre, y ahora el pago queda failed con su motivo en vez de pending. Ver si la respuesta vuelve pending.
  • El plazo por defecto de un pago con tarjeta baja de 60 a 15 minutos. Aplica a dinelco y bancard cuando no mandás expires_in, y coincide con lo que dura el formulario de tarjeta. Transferencia sigue sin vencimiento. Si necesitás más tiempo, mandá expires_in. Antes de vencer un pago con tarjeta le preguntamos al proveedor: si estaba aprobado te llega payment.confirmed, no payment.expired. Ver Expiración.
  • rejection_reason siempre presente en payment.failed. Los rechazos de Dinelco con checkout llegaban con rejection_reason: null aunque Dinelco mandara el motivo (por ejemplo, “El cliente canceló el challenge 3DS”). Ahora viene siempre, con el formato "<código>: <texto del proveedor>", o solo el texto si no hay código. También completamos el motivo de los pagos fallidos anteriores. Ver Eventos.
  • Un intento rechazado de Dinelco ya no cierra el pago al instante. Dentro del iframe de Dinelco el comprador puede tocar Reintentar pago después de un rechazo. Antes, el rechazo te mandaba payment.failed en el momento, y si el reintento se aprobaba te llegaba después un payment.confirmed marcado como recuperado (recovered_from_status: "failed"), aunque nunca hubiera salido de la ventana. Ahora el pago queda abierto mientras se puede reintentar: si se aprueba, te llega un solo payment.confirmed; si la sesión vence sin otro intento, te llega payment.failed con el motivo del rechazo, unos minutos después del intento (cuando vence la sesión de Dinelco). Ver múltiples webhooks.
  • Cierre automático de pagos sin respuesta. Si un proveedor nunca informa cómo terminó un pago, lo cerramos: los pagos con checkout vencen en su plazo, y un pago sin plazo pasa a failed a los 3 días, con un rejection_reason que lo explica. Si después aparece una aprobación, te llega payment.confirmed con recovered_from_status. Ver Pagos sin respuesta del proveedor.
  • return_url con el iframe embebido. Si embebés el checkout_url en un <iframe> y además mandaste return_url, al terminar redirigíamos adentro del iframe, y tu sitio aparecía cargado dentro del checkout. Ahora, cuando está embebido, el resultado llega siempre por postMessage, y el return_url se usa solo con el checkout abierto como página completa. Ver guía mobile.
  • Tarjetas de prueba de Dinelco. Las tarjetas sin desafío 3DS (terminadas en 1005 y 2235) sirven solo para pagos: Dinelco no deja catastrarlas. Para probar tarjetas guardadas usá las que tienen desafío. El código del desafío puede cambiar: si 1234 deja de funcionar, pedinos el vigente. Ver Entornos.

2026-09

  • Documentado el status=processing en la vuelta del comprador (2026-09-27). La API lo viene mandando, pero no estaba en ninguna de las dos tablas: la del checkout hospedado lo omitía y la de la guía mobile decía que los valores eran completed, failed o cancelled solamente. Si armaste tu handler con esas tablas, processing te está cayendo en la rama genérica. Significa que cuando el comprador volvió todavía no teníamos la respuesta del proveedor: puede haberse cobrado, y lo más común es que la confirmación llegue unos segundos después. El pago queda pending y el resultado llega por webhook (payment.confirmed, payment.failed, o payment.expired si nunca hay resultado). No le ofrezcas al comprador pagar de nuevo enseguida: si ya estaba aprobado, lo cobrarías dos veces. Corregido el mismo día: la primera versión de esta entrada decía que processing implicaba que no hubo cobro, y es al revés.
  • Tarjetas de prueba de Bancard (2026-09-27). Estaban las de Dinelco pero no las de Bancard. Ya están publicadas en Entornos.

2026-08

  • Cada intento de webhook se firma de nuevo (2026-08-30). Antes el X-Ingalca-Timestamp se acuñaba una sola vez y se reenviaba igual en todos los reintentos, así que un receptor que aplicara la ventana anti-replay de 300 s que recomendamos rechazaba todos los reintentos — y como un 4xx no se reintenta, eso cancelaba la entrega en forma definitiva y silenciosa. Ahora el timestamp y la firma se calculan en cada intento; el body se retransmite byte por byte. No tenés que cambiar nada: un timestamp fresco pasa cualquier ventana. Si habías ensanchado la tuya para compensar, ya podés volver a 300 s. Mismo arreglo para el reenvío manual desde el dashboard, que mandaba timestamp nuevo con la firma vieja y por lo tanto nunca validaba. Ver replay attacks.
  • Corregidos los tiempos de reintento (2026-08-30). La tabla publicada decía 1 min · 5 min · 30 min · 2 h pero el código nunca ejecutaba el primer escalón: los reintentos salían a los 5 min, 35 min, 2,6 h y 14,6 h. Alineamos el código a la tabla. Los 5 intentos ahora caen en t=0, 1 min, 6 min, 36 min y 2 h 36 min, y el último intento pasa de ~14,6 h a ~2,6 h del evento. Si dimensionaste tu reconciliación contra el comportamiento real anterior, ajustala. Ver reintentos.
  • expires_in en POST /v1/payments (2026-08-29). Los pagos directos ahora aceptan un plazo, igual que el checkout hospedado: mín 60 s, máx 86400. Vencido, el pago pasa a expired y recibís payment.expired. El default depende del proveedor: 3600 s para dinelco y bancard, y sin vencimiento para transferencia (el comprador va al banco y sube el comprobante horas después — ponerle un default rompería ese flujo). Un expires_in explícito gana siempre. Ver Expiración.
  • La página hospedada avisa y devuelve al comercio al vencer (2026-08-29). El checkout hospedado muestra un contador con el tiempo restante; al llegar a cero cierra el formulario de tarjeta, se lo explica al comprador y a los 5 segundos lo redirige a tu cancel_url con ?status=expired&reference=...&checkout_session_id=.... Antes se quedaba trabado en una página muerta. Si ya manejás la vuelta del comprador, agregá el caso expired — si no, va a caer en tu rama genérica de cancelación. Ver Expiración.
  • Cuatro campos nuevos en el payload de webhook (2026-08-29). Aditivos, no rompen nada: expires_at (el plazo del intento), expired_at (cuándo lo marcamos vencido), confirmed_after_expiry y recovered_from_status. Los dos últimos identifican una confirmación que llegó fuera de la ventana: el formulario de tarjeta vive en el dominio del proveedor y un pago puede aprobarse justo después del vencimiento. Si manejás stock o cupo, usalos para decidir si honrás el pago o lo revertís. Ver confirmaciones fuera de ventana.
  • Correcciones de documentación (2026-08-29). La doc de webhooks describía un payload con wrapper data y claves event_type / event_id en el body que nunca existió: el payload real es plano, el tipo viene en event, y el event_id solo en el header X-Ingalca-Event-Id. También estaba mal documentado que amount es un entero (es un string decimal, "150000.00") y que currency es opcional en POST /v1/payments (es requerido). El código no cambió — solo la documentación, que ahora refleja lo que la API viene devolviendo. Si escribiste tu handler siguiendo esos ejemplos, revisalo.
  • Checkout hospedado (2026-08-04). POST /v1/checkout-sessions crea una sesión de pago y te devuelve una url a la que redirigís el browser del comprador. Él elige método (Dinelco, Bancard o transferencia) y paga en una página hospedada por nosotros con tu logo y tus colores; vos no construís UI de pago ni embebés iframes. Al terminar vuelve a tu success_url / cancel_url, y la confirmación llega por webhook como siempre. Ver Checkout hospedado.
  • Bancard disponible como proveedor de pago (2026-08). POST /v1/payments acepta provider=bancard para cobro con tarjeta (pago ocasional): el cliente tipea la tarjeta dentro del iframe de Bancard —embebido en tu UI vía el checkout_url que te devolvemos, proxeado por el gateway— y el 3DS lo maneja Bancard. La confirmación llega por callback y te la reenviamos como payment.confirmed / payment.failed. La reversa (POST /v1/payments/{id}/reverse) funciona el mismo día, antes de la liquidación. Solo PYG. Las tarjetas guardadas con Bancard (catastro + cobro con token) llegan en una fase posterior. Ver Elegir método de pago.

2026-07

  • return_url con deep links + guía de integración móvil (2026-07-16). POST /v1/cards y POST /v1/payments aceptan ahora un campo return_url (opcional en integraciones web, obligatorio en apps móviles). Cuando termina el 3DS el proxy redirige a esa URL con ?status=X&card_id=Y (o payment_id). Acepta http/https además de esquemas custom (fixapp://, com.fix.app://) para integrar con SFSafariViewController (iOS) y Custom Tabs (Android) usando deep links — el patrón necesario porque los WebView modernos bloquean cookies third-party y rompen el 3DS. Sin return_url la comunicación sigue siendo postMessage como antes. Fix relacionado del mismo commit: POST /v1/cards ahora siempre envía un returnUrl válido a Dinelco (antes se colaba null cuando el integrador no lo mandaba y Dinelco cancelaba el 3DS con “cancelado por el cliente” aunque el OTP hubiera sido correcto). Ver guía mobile para ejemplos por stack.
  • checkout_url proxeado por el gateway (2026-07-16). POST /v1/cards (Dinelco) devuelve un nuevo campo registration.checkout_url que embebés como <iframe src> — servimos el HTML del checkout desde api.pay.ingalca.com y proxeamos al proveedor por atrás. Tu app nunca ve URLs ni tokens del proveedor. POST /v1/payments con Dinelco (sin card_id) suma un bloque data.checkout con el mismo shape. Los eventos del iframe llegan como postMessage normalizados en tu vocabulario: ingalca.registry.completed/.failed/.cancelled para catastro, ingalca.checkout.* para pagos. Los campos crudos anteriores (integrity_token, session_id, validate_url) siguen en la respuesta como internos — usá checkout_url. Ver guía de catastro y reference de pagos.
  • Eliminar tarjetas (borrado lógico) + dedup de catastros (2026-07-15). DELETE /v1/cards/{id} ahora responde 204 y hace un borrado lógico (la tarjeta queda deleted, sale del listado y no es cobrable); emitimos card.deleted. Además, re-catastrar la misma tarjeta ya no acumula duplicados: la registración anterior del mismo customer_id con igual número enmascarado/marca/tipo pasa a deleted automáticamente. Antes el DELETE devolvía 501. Dinelco no expone unregister remoto — si lo habilita, el DELETE lo va a usar sin cambiar el contrato.
  • Idempotencia por reference (2026-07-14). POST /v1/cards/{id}/charge y POST /v1/payments deduplican por reference: reenviar el mismo request (misma reference, mismo body) devuelve el pago original con 200, sin cobrar de nuevo; la misma reference con otro amount/card_id/currency devuelve 409 REFERENCE_ALREADY_USED. El replay devuelve el resultado original aunque haya terminado failed — para reintentar de verdad, usá una reference nueva. Ver idempotencia.

2026-05

  • target_origin acepta múltiples dominios (2026-05-12). Podés mandar target_origin como array en POST /v1/payments y POST /v1/cards para listar todos los orígenes (dominios/subdominios) desde donde podés embeber el iframe de Dinelco. La forma string (un solo origen) sigue funcionando. Ver Sobre target_origin.
  • Notificaciones por email configurables (2026-05-02). Te avisamos por email cuando un webhook agota reintentos o cuando un cliente sube un comprobante de transferencia para revisar. Configurás en Configuración → Webhooks → Notificaciones por email. Throttle: máx 1 email por hora (webhooks fallidos) / 30 min (comprobantes).
  • Aprobar/rechazar comprobantes vía API (2026-05). Endpoints POST /v1/payments/{id}/confirm y POST /v1/payments/{id}/reject para automatizar la aprobación de transferencias si tenés tu propia lógica.

2026-04

  • Customers como entidad referencial (2026-04). Endpoints GET /v1/customers y GET /v1/customers/{id}. Auto-create al crear pagos o tarjetas con customer.customerId. Webhook payment.* ahora incluye customer_id (external_id) en el payload. Ver guía de customers.
  • Reversa de pagos Dinelco (2026-04). POST /v1/payments/{id}/reverse para Dinelco antes del corte de liquidación. Webhook payment.reversed. Mensaje específico cuando ya pasó la ventana (responseCode: 90019). Ver guía de reversa.
  • Pago Failed (no Pending) cuando falla el create (2026-04). Antes los pagos con payload inválido quedaban como pending fantasma. Ahora se marcan failed con rejection_reason = mensaje del provider.
  • Sección /cards en el dashboard del tenant (2026-04). Listado read-only con drawer.

Si tenés un caso particular o preguntas sobre algún cambio, escribinos a desarrollo@ingalca.com.