Saltearse al contenido

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

Ventana de terminal
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:

  1. card.failed para card_xyz789 (primer intento cancelado o rechazado)
  2. card.registered para el mismo card_xyz789 unos 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

Ventana de terminal
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 paymentToken se guarda encriptado del lado nuestro y nunca se devuelve en respuestas. Vos solo manejás el card_id (card_xxx).
  • 3DS es obligatorio en el catastro de Dinelco — el cliente siempre pasa por el OTP.
  • El customerId que mandamos a Dinelco es nuestro public_id interno (cust_xxx), no el external_id que vos enviás. Esto evita colisiones cross-tenant en sandboxes compartidos.