Saltearse al contenido

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

CampoTipoNotas
idstringpay_xxxxxxxxxxxx. Inmutable.
referencestringTu referencia interna (orden, ticket, etc.).
providerenumdinelco | bancard | transferencia.
amountstringDecimal como string: "150000.00". No es un entero — casteá antes de comparar.
currencystringSolo PYG por ahora.
statusenumVer estados.
created_atdatetimeISO 8601 con timezone.
confirmed_atdatetime?Cuándo pasó a confirmed.
failed_atdatetime?Cuándo pasó a failed o rejected.
expires_atdatetime?El plazo: hasta cuándo aceptamos el intento. null si no vence. Ver Expiración.
expired_atdatetime?Cuándo pasó a expired. Ojo con el par de nombres: expires_at es el plazo, expired_at el hecho consumado.
confirmed_after_expirybooleanEl pago se confirmó fuera de su ventana. Ver Expiración.
recovered_from_statusstring?Estado del que se rescató el pago al confirmarlo tarde (expired, cancelled, failed).
rejection_reasonstring?Motivo si está en rejected.
metadataobject?Lo que vos mandaste al crear. Pasthru.
customer_dataobject?Snapshot inmutable del cliente al momento del cobro.
provider_dataobject?Datos específicos del proveedor (operation_number, etc.).

Estados (status)

EstadoSignificado
pendingCreado, esperando que el cliente complete el pago.
processingEsperando respuesta del proveedor (transitorio breve).
confirmedPagado con éxito.
failedEl proveedor rechazó el pago.
rejectedOperador rechazó manualmente un comprobante.
receipt_uploadedCliente subió comprobante de transferencia (esperando aprobación).
expiredSe agotó el plazo (expires_at) sin completarse. Ver Expiración.
cancelledCancelado vía API antes de completarse.
reversedPago revertido (Dinelco antes del corte).

Endpoints

Crear un pago

POST /v1/payments

Body:

{
"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:

CampoRequeridoNotas
providerdinelco | bancard | transferencia.
amountEntero positivo. Vuelve como string decimal en las respuestas.
currencySolo PYG. No tiene default: omitirlo devuelve 422.
referenceTu referencia única por operación. Es la clave de idempotencia — ver idempotencia.
target_originsi Dinelco (sin card_id)String o array. Origen(es) de tu app donde vas a embeber el iframe del checkout. Ver nota abajo.
return_urlno (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_urlnoURL a la que redirige el proxy si el comprador cancela o el pago falla. Si no la mandás, se usa return_url.
expires_innoSegundos de validez del intento. Mín 60, máx 86400. El default depende del proveedor — ver Expiración.
customerno (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_idnoAlternativa a customer: el external_id de un cliente que ya creaste. Ver Customers.
metadatanoObject libre. Lo devolvemos tal cual en respuestas y webhook.
card_idnoSi lo mandás, hace charge directo con esa card guardada (skipea iframe).

Respuesta201 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/payments

Query params:

ParamNotas
statusFiltrar por estado.
providerFiltrar por proveedor.
referenceBúsqueda parcial en reference y public_id.
customer_idpublic_id o external_id del customer.
from, toRango de fechas (ISO o YYYY-MM-DD).
cursor, per_pagePaginació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}/receipt

multipart/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}/cancel

Solo aplica a pagos pending.

Reversar

POST /v1/payments/{id}/reverse

Aplica 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

ProveedorDefault si no mandás expires_in
dinelco3600 s (60 min)
bancard3600 s (60 min)
transferenciaSin 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.