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
- Tu backend crea la sesión:
POST /v1/checkout-sessions→{ id, url, expires_at }. - Redirigís al comprador (browser) a
url. - 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.
- Al terminar vuelve a tu
success_url(pago hecho / comprobante subido) ocancel_url(canceló, falló o venció), con?status=...&reference=.... - Recibís el webhook
payment.confirmed(tarjeta) opayment.receipt_uploaded→ luegopayment.confirmed(transferencia, cuando aprobás el comprobante).
Valores de status en la vuelta
status | A dónde vuelve | Qué pasó |
|---|---|---|
completed | success_url | Pago con tarjeta aprobado. |
pending | success_url | Transferencia: subió el comprobante, falta que lo apruebes. |
failed | cancel_url | El proveedor rechazó el pago. |
cancelled | cancel_url | El comprador clickeó “Cancelar y volver”. |
expired | cancel_url | Se 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
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
| Campo | Requerido | Notas |
|---|---|---|
reference | sí | Tu identificador de orden. Idempotente mientras la sesión siga abierta: reenviarlo devuelve la misma sesión. Ojo con el detalle en Idempotencia. |
amount | sí | Monto total a cobrar (entero, PYG). En las respuestas y webhooks vuelve como string decimal ("110000.00"). |
currency | sí | Solo PYG. |
success_url | sí | A dónde vuelve el comprador al terminar. |
cancel_url | no | A dónde vuelve si cancela, falla o vence. Si no lo mandás, usamos success_url. |
methods | no | Subconjunto de ["dinelco","bancard","transferencia"]. Si lo omitís, se ofrecen todos los que tengas habilitados. Se intersecta con tus proveedores habilitados. |
expires_in | no | Segundos de validez. Default 3600 (60 min), mín 60, máx 86400. |
description | no | Título del cobro en la página. |
line_items | no | Desglose visible: [{ name, quantity, unit_amount }]. Es solo visual; el cobro es amount. |
metadata | no | Objeto libre. Se propaga al pago y al webhook. |
customer | no | Si 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 porX-Ingalca-Event-Id.checkout_session_id— elcs_...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_uploadedhasta que lo aprobás desde el dashboard → ahí llegapayment.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:
- Si tenía el formulario de tarjeta abierto, lo cerramos: desaparece de la página y no puede seguir tipeando ni enviar.
- Le explicamos que la sesión venció.
- A los 5 segundos lo mandamos de vuelta a tu
cancel_urlcon?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.