Pago con tarjeta tokenizada
Útil cuando tu cliente compra varias veces y no querés que retipee los datos de la tarjeta en cada compra. Después del catastro inicial, cada compra es un click: el cliente elige su tarjeta guardada y autoriza el cobro.
El flow tiene dos etapas: catastro (una vez por cliente, abre un iframe) y charge (cada cobro, server-to-server con el token guardado).
Diagrama del catastro
sequenceDiagram
autonumber
participant Cliente
participant TuApp as Tu app
participant Ingalca as INGALCA Pay
participant Dinelco
TuApp->>Ingalca: POST /v1/cards
{customer.customerId}
Ingalca->>Dinelco: Crear sesión registry
Dinelco-->>Ingalca: integrity_token + session_id
Ingalca-->>TuApp: card_xxx + checkout_url
TuApp-->>Cliente: Embed iframe (checkout_url)
Cliente->>Ingalca: Cargar checkout_url
Ingalca->>Dinelco: Proxeamos el iframe con el token
Cliente->>Dinelco: Tipear tarjeta + 3DS OTP
Dinelco-->>Ingalca: postMessage registry.success
Ingalca-->>TuApp: postMessage ingalca.registry.completed
Dinelco-->>Ingalca: Callback con paymentToken
Ingalca->>Ingalca: Marcar card registered
Ingalca->>TuApp: webhook card.registered (con token_id)
TuApp-->>Cliente: "Tarjeta guardada"
Paso 1 — iniciar catastro
curl -X POST https://api.pay.ingalca.com/v1/cards \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "provider": "dinelco", "target_origin": [ "https://tu-checkout.com", "https://app.tu-checkout.com" ], "customer": { "customerId": "user_42", "name": "Juan", "lastname": "Pérez", "email": "juan@example.com", "phone": "+595981234567" } }'target_origin lista los dominios de tu app donde vas a embeber el iframe. Los usamos para dirigir los eventos postMessage de vuelta a la ventana correcta. Aceptamos string (un origen) o array. No se aceptan wildcards — enumerá los subdominios que necesites.
Respuesta:
{ "data": { "card_id": "card_xyz789", "status": "pending", "registration": { "checkout_url": "https://api.pay.ingalca.com/v1/checkout-pages/eyJpdiI6...", "expires_at": "2026-05-02T15:00:00+00:00" } }}El checkout_url es un enlace de vida corta (~15 min) — si el cliente cierra el iframe sin completar, hacé POST /v1/cards de nuevo para obtener uno fresco.
Paso 2 — embeber el iframe
En tu frontend embebés directamente el checkout_url:
<iframe src="{checkout_url}" width="100%" height="600" style="border: 0" allow="payment"></iframe>Nosotros servimos el HTML del checkout desde api.pay.ingalca.com y proxeamos al proveedor por atrás — vos nunca ves datos sensibles ni URLs de Dinelco. El cliente tipea la tarjeta y pasa 3DS dentro del iframe.
Escuchar el resultado
El iframe emite postMessage con eventos ya normalizados en tu vocabulario, no en el del proveedor. Registrá un handler en la página que embebe el iframe:
window.addEventListener('message', (event) => { if (event.origin !== 'https://api.pay.ingalca.com') return;
const { type, card_id, reason } = event.data ?? {};
switch (type) { case 'ingalca.registry.completed': // Éxito: la tarjeta quedó registrada (o quedará al recibir el webhook). // Cerrá el iframe y esperá tu webhook card.registered para confirmar. break; case 'ingalca.registry.failed': // Dinelco rechazó el catastro; `reason` puede traer más contexto. break; case 'ingalca.registry.cancelled': // El cliente cerró el iframe o canceló. break; }});Paso 3 — recibir webhook de catastro
{ "event_type": "card.registered", "data": { "card_id": "card_xyz789", "customer_id": "user_42", "status": "registered", "provider": "dinelco", "brand": "Visa", "last_four": "1096", "expires": "12/30" }}Ya podés cobrar con esta card.
Múltiples webhooks para la misma tarjeta
Una única operación de catastro puede generar más de un webhook card.* para la misma card_id. El caso típico es cuando el cliente clickea el botón REINTENTAR dentro del iframe del proveedor después de un primer fallo — es un botón del proveedor, dentro de su misma sesión, así que reintenta con la misma card_id en vez de crear una nueva.
Secuencia real que puede llegar a tu endpoint:
card.failedparacard_xyz789(primer intento cancelado o rechazado)card.registeredpara el mismocard_xyz789unos minutos después (reintent exitoso)
El último evento manda. Tu handler tiene que tolerar transiciones de estado — no asumir que un card.failed es definitivo. Si guardaste la card como fallida en tu DB, un card.registered posterior sobre la misma card_id tiene que actualizar ese registro a activa (no crear uno nuevo).
El event_id sigue siendo la clave de idempotencia por evento (ver Reintentos e idempotencia). Lo que cambia acá es que los webhooks son un stream de estado sobre el recurso, no notificaciones one-shot — la fuente de verdad es la última palabra que recibiste sobre esa card_id.
Este comportamiento también aplica a payment.* con Dinelco (misma lógica de retry dentro del iframe), aunque en la práctica sale menos porque el flujo de pago único con checkout tiene menos oportunidades de retry visibles al cliente.
Diagrama del charge
sequenceDiagram
autonumber
participant TuApp as Tu app
participant Ingalca as INGALCA Pay
participant Dinelco
TuApp->>Ingalca: POST /v1/cards/{card_id}/charge
{amount, reference}
Ingalca->>Dinelco: POST api/v3/payment
(con tokenId)
Dinelco-->>Ingalca: status + responseCode
Ingalca-->>TuApp: payment confirmed o failed
Ingalca->>TuApp: webhook payment.confirmed o payment.failed
Paso 4 — cobrar
curl -X POST https://api.pay.ingalca.com/v1/cards/card_xyz789/charge \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "amount": 50000, "reference": "ORDEN-12345" }'A diferencia del checkout, el cobro es sincrónico — la respuesta ya tiene el resultado:
{ "data": { "id": "pay_def456", "status": "confirmed", "amount": 50000, "provider_data": { "operation_number": "0000123456", "authorization_number": "ABC123" } }}Estados de la tarjeta
stateDiagram-v2
[*] --> pending: POST /v1/cards
pending --> registered: callback OK + 3DS pasado
pending --> invalid: callback rechazado
registered --> deleted: DELETE /v1/cards/{id}*
invalid --> [*]
deleted --> [*]
* DELETE /v1/cards/{id} hace un borrado lógico (204): la tarjeta queda deleted y no se puede cobrar. Dinelco no expone unregister, así que su token persiste del lado de ellos pero queda inutilizable (los cobros solo se inician vía gateway sobre una tarjeta activa). Es una limitación del proveedor: si Dinelco habilita la baja remota, el DELETE la va a llamar para revocar el token de verdad, sin cambiar el contrato. Re-catastrar la misma tarjeta también manda la anterior a deleted automáticamente.
Notas
- El
paymentTokense guarda encriptado del lado nuestro y nunca se devuelve en respuestas. Vos solo manejás elcard_id(card_xxx). - 3DS es obligatorio en el catastro de Dinelco — el cliente siempre pasa por el OTP.
- El
customerIdque mandamos a Dinelco es nuestropublic_idinterno (cust_xxx), no elexternal_idque vos enviás. Esto evita colisiones cross-tenant en sandboxes compartidos.