Pagos
El objeto Payment
{ "id": "pay_abc123def456", "reference": "ORDEN-001", "provider": "dinelco", "amount": "150000.00", "currency": "PYG", "status": "confirmed", "created_at": "2026-05-02T14:30:00+00:00", "updated_at": "2026-05-02T14:32:11+00:00", "confirmed_at": "2026-05-02T14:32:11+00:00", "provider_approved_at": "2026-05-02T14:32:09+00:00", "failed_at": null, "expires_at": "2026-05-02T14:45:00+00:00", "expired_at": null, "confirmed_after_expiry": false, "recovered_from_status": null, "rejection_reason": null, "card": { "type": "credit", "brand": "visa", "masked_number": "445653******1096" }, "metadata": { "order_id": 1042 }, "customer_data": { "customerId": "user_42", "name": "Juan", "email": "juan@example.com" }, "provider_data": { "operation_number": "0000123456", "authorization_number": "ABC123", "response_code": "0000" }}Campos
| Campo | Tipo | Notas |
|---|---|---|
id | string | pay_xxxxxxxxxxxx. Inmutable. |
reference | string | Tu referencia interna (orden, ticket, etc.). |
provider | enum | dinelco | bancard | transferencia. |
amount | string | Decimal como string: "150000.00". No es un entero — casteá antes de comparar. |
currency | string | Solo PYG por ahora. |
status | enum | Ver estados. |
created_at | datetime | ISO 8601 con timezone. |
confirmed_at | datetime? | Cuándo lo confirmamos nosotros (pasó a confirmed y salió el webhook). |
provider_approved_at | datetime? | Cuándo aprobó el proveedor la transacción, según el proveedor. Casi siempre unos segundos antes de confirmed_at; si se perdió un aviso puede ser bastante antes. Dinelco lo informa; con Bancard es null. Ver confirmaciones fuera de ventana. |
failed_at | datetime? | Cuándo pasó a failed o rejected. |
expires_at | datetime? | El plazo: hasta cuándo aceptamos el intento. null si no vence. Ver Expiración. |
expired_at | datetime? | Cuándo pasó a expired. Ojo con el par de nombres: expires_at es el plazo, expired_at el hecho consumado. |
confirmed_after_expiry | boolean | El pago se confirmó fuera de su ventana. Ver Expiración. |
recovered_from_status | string? | Estado del que se rescató el pago al confirmarlo tarde (expired, cancelled, failed). |
rejection_reason | string? | Motivo si está en failed o rejected. En un rechazo de tarjeta: "<código>: <texto del proveedor>", o solo el texto si el proveedor no manda código. |
card | object? | Con qué tarjeta se pagó (o se intentó pagar). null en transferencias o mientras no hay resultado. Ver tarjeta usada. |
metadata | object? | Lo que vos mandaste al crear. Pasthru. |
customer_data | object? | Snapshot inmutable del cliente al momento del cobro. |
provider_data | object? | Datos específicos del proveedor (operation_number, código de autorización, etc.). Ver datos del proveedor. |
Tarjeta usada
card dice con qué tarjeta se pagó, o se intentó pagar: también viene en un pago failed.
| Campo | Valores | Dinelco | Bancard |
|---|---|---|---|
type | credit | debit | Sí | Sí, ver la nota |
brand | visa, mastercard, diners, … | Sí | null |
masked_number | Número enmascarado, ej. 445653******1096 | Sí | null |
Datos del proveedor (provider_data)
Lo que informó el proveedor sobre ese intento. Sirve para mostrarle el comprobante al cliente (en el pedido, en un PDF) y para conciliar con el proveedor.
Un pago con Bancard aprobado:
"provider_data": { "provider_reference": null, "provider_status": "S", "authorization_number": "654321", "ticket_number": "1234567890", "response_code": "00", "response_description": "Transaccion aprobada"}| Campo | Dinelco | Bancard | Notas |
|---|---|---|---|
provider_reference | Sí | Sí | Siempre presente; puede ser null. |
provider_status | Sí | Sí | Estado crudo del proveedor (Dinelco: SUCCESS, FAILED…; Bancard: S/N). No lo uses para decidir: para eso está status. Siempre presente; puede ser null. |
session_id | Sí | — | Sesión de checkout de Dinelco. |
operation_number | Sí | — | Número de operación de Dinelco (“Boleta N°”). |
authorization_number | Sí | Sí | Código de autorización de la transacción. |
ticket_number | — | Sí | Número de ticket de Bancard. |
response_code | Sí | Sí | Código de respuesta del proveedor. Con Bancard, 00 es aprobado. |
response_description | — | Sí | Texto del proveedor para response_code, ej. "Transaccion aprobada". |
Salvo provider_reference y provider_status, cada clave viene solo si el proveedor la informó: leelas como opcionales. En un pago que todavía no tiene resultado, o en uno rechazado antes de llegar al procesador, pueden faltar. Excepción por compatibilidad: en un pago de Dinelco con session_id, operation_number y authorization_number vienen siempre, en null si no hay dato.
En transferencias, provider_data trae las cuentas bancarias (bank_accounts) y las instructions para el cliente.
El webhook payment.* trae los mismos datos del proveedor; ver eventos.
Estados (status)
| Estado | Significado |
|---|---|
pending | Creado, esperando que el cliente complete el pago. |
processing | Esperando respuesta del proveedor (transitorio breve). |
confirmed | Pagado con éxito. |
failed | El proveedor rechazó el pago. |
rejected | Operador rechazó manualmente un comprobante. |
receipt_uploaded | Cliente subió comprobante de transferencia (esperando aprobación). |
expired | Se agotó el plazo (expires_at) sin completarse. Ver Expiración. |
cancelled | Cancelado vía API antes de completarse. |
reversed | Pago revertido (Dinelco antes del corte). |
Endpoints
Crear un pago
POST /v1/paymentsBody:
{ "provider": "transferencia", "amount": 150000, "currency": "PYG", "reference": "ORDEN-001", "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" }, "expires_in": 1200, "metadata": { "order_id": 1042 }, "card_id": "card_xyz789"}Campos:
| Campo | Requerido | Notas |
|---|---|---|
provider | sí | dinelco | bancard | transferencia. |
amount | sí | Entero positivo. Vuelve como string decimal en las respuestas. |
currency | sí | Solo PYG. No tiene default: omitirlo devuelve 422. |
reference | sí | Tu referencia única por operación. Es la clave de idempotencia — ver idempotencia. |
target_origin | si Dinelco (sin card_id) | String o array. Origen(es) de tu app donde vas a embeber el iframe del checkout. Ver nota abajo. |
return_url | no (recomendado en mobile) | URL a la que redirige el proxy al terminar el 3DS, con ?status=X&payment_id=Y. Acepta http/https o esquemas custom (fixapp://), salvo javascript:, data:, vbscript:, file:, blob: y about:, que responden 422. Obligatorio si integrás desde una app móvil o PWA — ver guía mobile. Sin este campo, la comunicación es por postMessage al parent del iframe. |
cancel_url | no | URL a la que redirige el proxy si el comprador cancela o el pago falla. Si no la mandás, se usa return_url. |
expires_in | no | Segundos de validez del intento. Mín 60, máx 86400. El default depende del proveedor — ver Expiración. |
customer | no (todo o nada) | Datos del cliente. Si lo mandás con Dinelco, van los 5 campos: customerId (tu external_id), name, lastname, email, phone. Mandarlo incompleto devuelve 422. Omitilo entero si no lo necesitás. |
customer_id | no | Alternativa a customer: el external_id de un cliente que ya creaste. Ver Customers. |
metadata | no | Object libre. Lo devolvemos tal cual en respuestas y webhook. |
card_id | no | Si lo mandás, hace charge directo con esa card guardada (skipea iframe). |
Respuesta — 201 Created:
Cuando el pago requiere un iframe (Dinelco sin card_id), la respuesta incluye un bloque checkout con el enlace del iframe:
{ "data": { "id": "pay_abc123", "status": "pending", "expires_at": "2026-05-02T14:57:00+00:00", "checkout": { "checkout_url": "https://api.pay.ingalca.com/v1/checkout-pages/eyJpdiI6...", "expires_at": "2026-05-02T14:47:00+00:00" } }}La respuesta es el objeto Payment completo, más el bloque checkout. El ejemplo muestra solo los campos de esta sección. data.expires_at viene siempre (es el plazo del pago: el expires_in que pediste, o 15 minutos por defecto con tarjeta), y es distinto de checkout.expires_at (ver abajo).
Con checkout_url embebés el iframe del checkout en tu frontend:
<iframe src="{checkout_url}" allow="payment" style="width:100%;height:600px;border:0"></iframe>Nosotros servimos el HTML desde api.pay.ingalca.com y proxeamos al proveedor por atrás — tu app nunca ve URLs ni datos del proveedor. Escuchá los eventos postMessage normalizados:
window.addEventListener('message', (event) => { if (event.origin !== 'https://api.pay.ingalca.com') return; const { type, payment_id, reason } = event.data ?? {}; // type: 'ingalca.checkout.completed' | '.failed' | '.cancelled' | '.expired'});El estado autoritativo del pago llega por webhook payment.confirmed / payment.failed — el postMessage solo te dice cómo cerró la interacción visual.
Al llegar el plazo del pago, la página del checkout_url saca el formulario y muestra que el plazo venció: el comprador ya no puede pagar desde ahí. Te avisa con postMessage ingalca.checkout.expired si está embebida, o redirige a tu return_url con status=expired si está abierta como página completa. Lo mismo pasa si cancelás el pago con POST /v1/payments/{id}/cancel: en hasta 5 segundos la página saca el formulario y avisa ingalca.checkout.cancelled. Si el comprador abre el checkout_url de un pago ya cancelado o vencido, no ve el formulario.
Cuando el pago no requiere iframe (transferencia, o Dinelco con card_id — cobro con tarjeta ya catastrada), el bloque checkout no aparece: transferencia devuelve las cuentas bancarias en provider_data, y el charge con tarjeta guardada resuelve sincrónico a confirmed/failed.
Sobre target_origin
Lista los orígenes de tu app donde vas a embeber el iframe del checkout. Los usamos para dirigir los postMessage de vuelta a la ventana correcta.
- Aceptamos string (un origen) o array (varios).
- Cada item debe ser una URL absoluta (
https://dominio.com), sin path ni trailing slash. - Wildcards no se soportan. Si tenés subdominios dinámicos (por sucursal, por cliente, etc.) tenés que enumerarlos uno por uno.
- Es opcional para transferencia y para cobros con
card_id(no hay iframe que embeber en esos flujos).
Ejemplo si tu checkout vive en https://checkout.tudominio.com y también atendés desde una app móvil que abre https://m.tudominio.com:
"target_origin": [ "https://checkout.tudominio.com", "https://m.tudominio.com"]Si te falta declarar un origen, los postMessage del iframe no llegan a tu página. El pago igual puede resolverse (el webhook llega server-to-server), pero tu UI no se entera al instante.
Listar pagos
GET /v1/paymentsQuery params:
| Param | Notas |
|---|---|
status | Filtrar por estado. |
provider | Filtrar por proveedor. |
reference | Búsqueda parcial en reference y public_id. |
customer_id | public_id o external_id del customer. |
from, to | Rango de fechas (ISO o YYYY-MM-DD). |
cursor, per_page | Paginación. |
Obtener un pago
GET /v1/payments/{id}Devuelve el objeto completo, incluyendo provider_data y comprobantes (receipts[]) si es de transferencia.
Subir comprobante (transferencia)
POST /v1/payments/{id}/receiptmultipart/form-data con campo file (jpg/png/pdf, max 10MB) y opcional notes.
Aprobar / rechazar (transferencia)
POST /v1/payments/{id}/confirm # body opcional: {"notes": "..."}POST /v1/payments/{id}/reject # body: {"reason": "..."}Cancelar
POST /v1/payments/{id}/cancelSolo aplica a pagos pending. Si el pago ya no está pendiente (por ejemplo, porque se aprobó en ese mismo momento), responde 422 y el pago queda como estaba: consultá su estado.
Cancelar cierra el pago en INGALCA Pay. Dinelco y Bancard no permiten anular un formulario de tarjeta ya abierto. Por eso:
- Si el comprador tiene abierto el
checkout_url, en hasta 5 segundos la página saca el formulario y avisaingalca.checkout.cancelled, o vuelve a tureturn_urlconstatus=cancelled. Si lo abre después de que cancelaste, ya no ve el formulario. - Un pago cancelado puede terminar aprobado. Si el comprador envió la tarjeta antes de que la página sacara el formulario, el proveedor cobra igual. Te llega
payment.confirmedconrecovered_from_status: "cancelled", aunque ya te hubiéramos mandadopayment.cancelled. Decidí si entregás lo que compró o devolvés la plata con Reversar, que en Dinelco solo funciona antes del corte de liquidación.
Reversar
POST /v1/payments/{id}/reverseAplica a pagos Dinelco y Bancard confirmados, antes del corte de liquidación de la marca. Ver guía de reversa.
Expiración
expires_in define por cuántos segundos aceptamos el intento. Usalo cuando tenés stock, cupo o un precio con vigencia: mandá los mismos minutos que dura tu reserva y las dos ventanas quedan sincronizadas.
Vencido el plazo, el pago pasa a expired, te llega un payment.expired, y dejamos de aceptarlo por nuestros canales.
Default por proveedor
| Proveedor | Default si no mandás expires_in |
|---|---|
dinelco | 900 s (15 min) |
bancard | 900 s (15 min) |
transferencia | Sin vencimiento |
La asimetría es deliberada. Un pago con tarjeta es una sesión de browser: el comprador tipea la tarjeta ahora o no la tipea. Una transferencia no — va al banco y sube el comprobante horas o días después, y ese es el flujo normal. Ponerle un default lo rompería.
Un expires_in explícito gana siempre, también en transferencia: si querés que tus transferencias venzan, mandalo.
expires_in no aplica a POST /v1/cards/{id}/charge: el cobro se resuelve en el mismo request o, si el proveedor no responde a tiempo, en minutos (ver cobrar con tarjeta guardada).
Pagos sin respuesta del proveedor
Si el proveedor nunca nos informa cómo terminó un pago, lo seguimos consultando. Cuando eso no alcanza:
- un pago con checkout pasa a
expiredal vencer su plazo. Si el comprador intentó pagar y el proveedor lo rechazó (por ejemplo, canceló el 3DS) y no volvió a intentar, pasa afailedcon el motivo de ese rechazo; - un cobro con tarjeta guardada pasa a
faileda los 10 minutos, conrejection_reason: "El proveedor no informó el resultado del cobro dentro del plazo."; - un pago sin plazo se cierra como
faileda los 3 días, conrejection_reason: "Cerrado automáticamente: el proveedor no informó el resultado en 3 días.".
En todos los casos, si después el proveedor informa una aprobación, te llega payment.confirmed con recovered_from_status (ver confirmaciones fuera de ventana).
Qué hace y qué no hace
Al vencer cerramos todo lo que controlamos: la página hospedada y la del checkout_url sacan el formulario de tarjeta aunque el comprador lo tenga abierto, dejamos de emitir iframes nuevos, y nuestros endpoints (como la subida de comprobante) devuelven 422.
Tampoco podemos evitar que el comprador transfiera plata a tu cuenta después del plazo — solo rechazamos la subida del comprobante. Y POST /v1/payments/{id}/confirm sigue funcionando sobre un pago vencido, a propósito: si recibiste la transferencia en tu cuenta, el plazo no la hace desaparecer.