Saltearse al contenido

Checkout hospedado (redirect)

El checkout hospedado te deja cobrar sin construir la UI de pago ni embeber ningún iframe en tu dominio. Creás una CheckoutSession por API, redirigís al comprador a la url que te devolvemos, y él elige el método (tarjeta con Dinelco o Bancard, o transferencia), paga en una página hospedada por nosotros con tu marca, y vuelve a tu success_url / cancel_url. La confirmación real llega, como siempre, por webhook.

Es la opción recomendada cuando la compra se inicia desde una web o app externa y no querés hospedar el iframe vos mismo.

Flujo

  1. Tu backend crea la sesión: POST /v1/checkout-sessions{ id, url, expires_at }.
  2. Redirigís al comprador (browser) a url.
  3. El comprador elige método y paga en la página hospedada. Si solo hay un método habilitado, se salta el selector y va directo.
  4. Al terminar vuelve a tu success_url (pago hecho / comprobante subido) o cancel_url (canceló, falló o venció), con ?status=...&reference=....
  5. Recibís el webhook payment.confirmed (tarjeta) o payment.receipt_uploaded → luego payment.confirmed (transferencia, cuando aprobás el comprobante).

Valores de status en la vuelta

statusA dónde vuelveQué pasó
completedsuccess_urlPago con tarjeta aprobado.
pendingsuccess_urlTransferencia: subió el comprobante, falta que lo apruebes.
failedcancel_urlEl proveedor rechazó el pago.
cancelledcancel_urlEl comprador clickeó “Cancelar y volver”.
expiredcancel_urlSe agotó el plazo de la sesión. Ver Expiración.

También mandamos reference (el de tu orden) y checkout_session_id.

Si no mandás cancel_url, todos esos casos vuelven a success_url, así que ahí el chequeo de status es imprescindible.

Crear una sesión

Ventana de terminal
curl -X POST https://api.pay.ingalca.com/v1/checkout-sessions \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"reference": "ORDEN-1042",
"amount": 110000,
"currency": "PYG",
"methods": ["dinelco", "bancard", "transferencia"],
"success_url": "https://tuweb.com/gracias",
"cancel_url": "https://tuweb.com/carrito",
"expires_in": 3600,
"description": "Entradas Torneopolis — Evento X",
"line_items": [
{ "name": "Entrada General", "quantity": 2, "unit_amount": 50000 },
{ "name": "Cargo de servicio", "quantity": 1, "unit_amount": 10000 }
],
"metadata": { "order_id": 1042 },
"customer": {
"customerId": "user_42",
"name": "Juan",
"lastname": "Pérez",
"email": "juan@example.com",
"phone": "+595981234567"
}
}'

Respuesta (201 Created):

{
"data": {
"id": "cs_9f3a...",
"url": "https://api.pay.ingalca.com/v1/checkout/cs_9f3a...",
"status": "open",
"reference": "ORDEN-1042",
"amount": "110000.00",
"currency": "PYG",
"methods": ["dinelco", "bancard", "transferencia"],
"expires_at": "2026-08-04T16:00:00+00:00"
}
}

Redirigí al comprador a data.url.

Campos

CampoRequeridoNotas
referenceTu identificador de orden. Idempotente mientras la sesión siga abierta: reenviarlo devuelve la misma sesión. Ojo con el detalle en Idempotencia.
amountMonto total a cobrar (entero, PYG). En las respuestas y webhooks vuelve como string decimal ("110000.00").
currencySolo PYG.
success_urlA dónde vuelve el comprador al terminar.
cancel_urlnoA dónde vuelve si cancela, falla o vence. Si no lo mandás, usamos success_url.
methodsnoSubconjunto de ["dinelco","bancard","transferencia"]. Si lo omitís, se ofrecen todos los que tengas habilitados. Se intersecta con tus proveedores habilitados.
expires_innoSegundos de validez. Default 3600 (60 min), mín 60, máx 86400.
descriptionnoTítulo del cobro en la página.
line_itemsnoDesglose visible: [{ name, quantity, unit_amount }]. Es solo visual; el cobro es amount.
metadatanoObjeto libre. Se propaga al pago y al webhook.
customernoSi lo mandás y Dinelco es un método posible, deben venir los 5 campos (customerId, name, lastname, email, phone).

Conciliación: reference, payment_id y método

Una sesión puede generar varios intentos de pago (el comprador reintenta, o cambia de método). Cada intento es un payment con su propio payment_id, pero todos llevan tu reference de orden. En el webhook recibís:

  • reference — el de tu orden (el de la sesión). Conciliá por acá.
  • payment_id — distinto por intento. Deduplicá los eventos por X-Ingalca-Event-Id.
  • checkout_session_id — el cs_... de la sesión.
  • payment_method — el método usado en ese intento (dinelco / bancard / transferencia).
  • metadata — la que mandaste al crear la sesión.
{
"event": "payment.confirmed",
"payment_id": "pay_7b1c...",
"reference": "ORDEN-1042",
"checkout_session_id": "cs_9f3a...",
"payment_method": "dinelco",
"amount": "110000.00",
"currency": "PYG",
"metadata": { "order_id": 1042 }
}

Verificá la firma como siempre: Verificar firma HMAC.

Branding

La página de pago usa el logo, nombre comercial y color que configurás en el dashboard (Configuración → Empresa → Branding del checkout). Si no configurás nada, usamos el nombre del comercio y un color por defecto.

Métodos

  • Tarjeta (Dinelco): el comprador tipea la tarjeta en el formulario seguro (3DS). Confirmación en segundos por webhook payment.confirmed.
  • Tarjeta (Bancard): igual que Dinelco desde el lado del comprador — tipea la tarjeta en el iframe de Bancard (3DS incluido). La confirmación llega por callback de Bancard y te la reenviamos como payment.confirmed / payment.failed, típicamente en segundos.
  • Transferencia: mostramos tus cuentas bancarias y el comprador sube el comprobante. Queda en receipt_uploaded hasta que lo aprobás desde el dashboard → ahí llega payment.confirmed.

Si habilitás Dinelco y Bancard a la vez, el comprador ve dos opciones de tarjeta y elige. Lo habitual es habilitar uno solo de los dos como método de tarjeta.

Expiración

Usá expires_in para que la ventana de pago coincida con la que tenés en tu sistema. Si reservás stock, cupo o congelás un precio por N minutos, mandá esos mismos N minutos y las dos ventanas quedan sincronizadas.

Qué ve el comprador

La página muestra un contador visible con el tiempo restante. Al llegar a cero:

  1. Si tenía el formulario de tarjeta abierto, lo cerramos: desaparece de la página y no puede seguir tipeando ni enviar.
  2. Le explicamos que la sesión venció.
  3. A los 5 segundos lo mandamos de vuelta a tu cancel_url con ?status=expired&reference=...&checkout_session_id=....

Los 5 segundos son para que alcance a leer qué pasó. También hay un botón “Volver al comercio” por si tiene JavaScript deshabilitado.

Con eso recuperás el control: en tu cancel_url revalidás precio y disponibilidad, y le avisás si algo cambió mientras pagaba.

Si abre el link cuando la sesión ya estaba vencida, la página responde 410 con el mismo mensaje y el mismo redirect.

Con menos de 60 segundos restantes no dejamos arrancar un pago con tarjeta: no alcanza para completarlo y solo consumiría un intento contra el proveedor.

Qué recibís vos

Cada intento pendiente de la sesión pasa a expired y te llega un payment.expired.

Si la sesión vence sin que el comprador haya elegido un método, no hay ningún pago creado y por lo tanto no recibís ningún webhook. Para esos casos apoyate en tu propio vencimiento, o en el status=expired de la vuelta.

Lo que la expiración NO garantiza