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",
"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

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 lo confirmamos nosotros (pasó a confirmed y salió el webhook).
provider_approved_atdatetime?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_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 failed o rejected. En un rechazo de tarjeta: "<código>: <texto del proveedor>", o solo el texto si el proveedor no manda código.
cardobject?Con qué tarjeta se pagó (o se intentó pagar). null en transferencias o mientras no hay resultado. Ver tarjeta usada.
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, 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.

CampoValoresDinelcoBancard
typecredit | debitSíSí, ver la nota
brandvisa, mastercard, diners, …Sínull
masked_numberNúmero enmascarado, ej. 445653******1096Sí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"
}
CampoDinelcoBancardNotas
provider_referenceSíSíSiempre presente; puede ser null.
provider_statusSí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_idSí—Sesión de checkout de Dinelco.
operation_numberSí—Número de operación de Dinelco (“Boleta N°”).
authorization_numberSíSíCódigo de autorización de la transacción.
ticket_number—SíNúmero de ticket de Bancard.
response_codeSí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)

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
providersídinelco | bancard | transferencia.
amountsíEntero positivo. Vuelve como string decimal en las respuestas.
currencysíSolo PYG. No tiene default: omitirlo devuelve 422.
referencesíTu 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://), 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_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).

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/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. 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 avisa ingalca.checkout.cancelled, o vuelve a tu return_url con status=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.confirmed con recovered_from_status: "cancelled", aunque ya te hubiéramos mandado payment.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}/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
dinelco900 s (15 min)
bancard900 s (15 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: 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 expired al vencer su plazo. Si el comprador intentó pagar y el proveedor lo rechazó (por ejemplo, canceló el 3DS) y no volvió a intentar, pasa a failed con el motivo de ese rechazo;
  • un cobro con tarjeta guardada pasa a failed a los 10 minutos, con rejection_reason: "El proveedor no informó el resultado del cobro dentro del plazo.";
  • un pago sin plazo se cierra como failed a los 3 días, con rejection_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.