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": "card.registered", "card_id": "card_xyz789", "customer_id": "user_42", "provider": "dinelco", "status": "registered", "masked_number": "445653******1096", "brand": "visa", "card_type": "credit", "expiration": null, "created_at": "2026-05-02T14:32:10+00:00", "last_synced_at": "2026-05-02T14:32:10+00:00", "timestamp": "2026-05-02T14:32:11+00:00"}El payload es plano, igual que el de los eventos payment.*: el tipo de evento viene en event y los campos van al nivel superior, sin wrapper data. Los últimos 4 dígitos salen de masked_number. Con Dinelco, expiration llega en null porque el proveedor no informa el vencimiento. Referencia completa en Eventos.
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.
En los pagos con checkout de Dinelco el iframe también tiene un botón Reintentar pago, pero ahí no vas a recibir eventos intermedios. Un intento rechazado no cierra el pago mientras el comprador todavía puede reintentar. Te llega un solo evento final:
payment.confirmedsi un reintento se aprueba;payment.failed, con el motivo del último rechazo enrejection_reason, si la sesión de Dinelco vence sin que nadie vuelva a pagar;payment.expiredsi el comprador nunca intentó pagar.
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 (o pending si Dinelco no respondió a tiempo)
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 se resuelve en el mismo request — casi siempre la respuesta ya tiene el resultado:
{ "data": { "id": "pay_def456", "status": "confirmed", "amount": 50000, "provider_data": { "operation_number": "0000123456", "authorization_number": "ABC123" } }}Si la respuesta vuelve pending
A veces Dinelco tarda más de lo normal en procesar el cobro (llegamos a ver más de 30 segundos). Si no nos responde a tiempo, no te devolvemos un error: te devolvemos el pago en pending, con su id, porque Dinelco pudo haberlo cobrado.
{ "data": { "id": "pay_def456", "status": "pending", "amount": 50000 }}Qué hacer:
- Mostrale al cliente “procesando” y esperá el webhook. En uno o dos minutos le preguntamos a Dinelco cómo terminó y te llega
payment.confirmedopayment.failed(con el motivo enrejection_reason). - No reintentes con otra
reference: si el primer cobro se aprobó, cobrarías dos veces. Si reenviás el request con la mismareference, te devolvemos el mismo pago sin volver a cobrar. - Si en 10 minutos Dinelco sigue sin informar el resultado, el pago pasa a
failedconrejection_reason: "El proveedor no informó el resultado del cobro dentro del plazo.". Si más tarde aparece una aprobación, te llegapayment.confirmedconrecovered_from_status: "failed".
Si el cobro seguro no se hizo (no pudimos conectar con Dinelco, o Dinelco rechazó el pedido), la respuesta es un error como siempre y el pago queda failed con el motivo.
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.