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", "failed_at": null, "expires_at": "2026-05-02T15:30:00+00:00", "expired_at": null, "confirmed_after_expiry": false, "recovered_from_status": null, "rejection_reason": null, "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 pasó a confirmed. |
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 rejected. |
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, etc.). |
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://). 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", "checkout": { "checkout_url": "https://api.pay.ingalca.com/v1/checkout-pages/eyJpdiI6...", "expires_at": "2026-05-02T14:47:00+00:00" } }}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'});El checkout_url vive ~15 minutos (expires_at); si el cliente cierra el iframe sin completar, hacé POST /v1/payments de nuevo para uno fresco. El estado autoritativo del pago llega por webhook payment.confirmed / payment.failed — el postMessage solo te dice cómo cerró la interacción visual.
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.
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 | 3600 s (60 min) |
bancard | 3600 s (60 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: ahí el pago se crea y se cobra en el mismo request.
Qué hace y qué no hace
Al vencer cerramos todo lo que controlamos: la página hospedada se cierra, 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.