Checkout y pago
Lleva al comprador a pagar, recíbelo de vuelta y confirma el pago con Storefront API.
Andrea confirma su carrito y elige pagar con PowerPay. El checkout crea el pedido e inicia el pago; tu tienda la lleva a pagar, la recibe de vuelta y consulta si pagó.
POST / v1/ carts/ {cartToken}/ checkoutpayment con action.urlaction.url/ orders/ {statusToken}/ payment-returnGET / v1/ orders/ status/ {statusToken}step y balanceDueCómo se paga
El método de pago decide qué hace tu tienda:
- Con redirección. El comprador paga en la página de la pasarela y vuelve a tu tienda.
- Con formulario. Montas el formulario de la pasarela en tu checkout.
- Manual. Transferencia, Yape o contra entrega. Muestras las instrucciones y el comercio confirma el pago.
En los tres casos, la pasarela o el comercio confirman el pago. Tu tienda consulta el estado.
Requisitos
- Un carrito listo para el checkout. Ver Tienda headless.
- Tu frontend con HTTPS en el dominio principal de la tienda.
GET /lo marca conv1/ stores/ domains isPrimary: true. - Las rutas
/yorders/ {statusToken} /en ese dominio.orders/ {statusToken}/ payment-return - Para confirmar desde tu sistema, un token de Admin con el permiso
payment:createenECOMIQ_TOKEN. Ver Inicio rápido. - Node.js o Python con
requests, para los ejemplos.
Lee el pago
POST / crea el pedido e inicia el pago. La
respuesta trae el pago en payment. Este ejemplo paga con PowerPay, que cobra
fuera de tu tienda.
curl -X POST https://storefront.ecomiq.pe/v1/carts/<cart-token>/checkout \
-H "x-Store-Domain: mi-tienda" \
-H "Content-Type: application/json" \
-d '{ "paymentMethodCode": "powerpay" }'{
"orderId": "7204558912004325350",
"orderNumber": "ORD-2026-0001",
"orderGroup": "7204558912004325350",
"orderIds": ["7204558912004325350"],
"payment": {
"transactionId": "7204558912004325362",
"paymentMethodCode": "powerpay",
"providerId": "powerpay",
"status": "pending",
"amount": 170.0,
"reference": "b6f1c2d4-3e5a-4f7b-8c9d-0e1f2a3b4c5d",
"actionKind": "redirect",
"receiptUrl": null,
"action": {
"publicConfiguration": {},
"url": "https://pasarela.example.com/pagar/b6f1c2d4",
"html": null,
"clientSecret": null,
"qrCode": null,
"expiresAt": "2026-10-05T16:00:00Z"
}
}
}La respuesta también trae cart, ya en estado completed, y orderStatusUrl,
la página de estado del pedido, que termina en /. Guarda
orderId, orderNumber, el cartToken y el statusToken: los usas para
consultar el estado. Los pasos siguientes reutilizan API y headers.
| Campo | Qué lleva |
|---|---|
transactionId | ID del pago. Lo usas para confirmarlo desde tu sistema. |
paymentMethodCode | Método elegido en el checkout. |
providerId | Pasarela que cobra. manual si no hay pasarela. |
status | Estado del pago. |
amount | Monto a cobrar: el total de todos los pedidos del carrito. |
reference | Referencia de la pasarela, tu externalRef o el transactionId, en ese orden. |
actionKind | Qué hace el comprador para pagar. Ver Continúa el pago. |
receiptUrl | Comprobante adjunto, si lo enviaste. |
action | Datos para continuar el pago. null si no hace falta nada. |
status | Qué significa |
|---|---|
pending | Falta que el comprador pague o que el comercio confirme el pago. |
authorized | La pasarela reservó el monto. Falta capturarlo. |
captured | Pago cobrado. El pedido queda confirmed. |
failed | La pasarela rechazó el pago. |
cancelled | El pago se canceló. |
refunded | El pago se devolvió. |
Si el total es 0, payment es null y el pedido queda confirmed. Mientras el
pedido siga activo, repetir el checkout del mismo carrito devuelve el mismo
pedido y su pago.
Continúa el pago
actionKind dice qué hacer con el comprador. action trae los datos.
actionKind | Qué hacer |
|---|---|
redirect | Redirige al comprador a action.url. Si viene action.expiresAt, el enlace vence entonces. |
embedded | Monta el formulario de la pasarela en tu página con action.clientSecret o action.html, y las claves públicas de action.publicConfiguration. |
qrCode | Muestra el QR de action.qrCode. Con providerId: "manual", es la URL de una imagen. |
voucher | Muestra el código de action.voucherCode y la imagen del QR de action.qrImageUrl. El comprador paga fuera del checkout. |
threeDS | Autentica la tarjeta. Ver Tarjeta tokenizada y autenticación 3DS. |
none | Si status es captured, muestra la confirmación. Si no, muestra las instrucciones del método: el comercio confirma el pago. |
function continuePayment(payment) {
if (payment === null || payment.status === "captured") {
return showConfirmation();
}
if (payment.actionKind === "redirect") {
return window.location.assign(payment.action.url);
}
if (payment.actionKind === "embedded") {
return mountProviderForm(payment.action);
}
if (payment.actionKind === "qrCode") {
return showQr(payment.action.qrCode);
}
if (payment.actionKind === "voucher") {
return showVoucher(payment.action.voucherCode, payment.action.qrImageUrl);
}
if (payment.actionKind === "threeDS") {
return authenticateCard(payment);
}
return showInstructions(payment.paymentMethodCode, payment.action);
}
continuePayment(checkout.payment);showConfirmation, mountProviderForm, showQr, showVoucher,
authenticateCard y showInstructions son funciones de tu tienda. En Python, / muestra el formulario, el QR
o las instrucciones.
Muestra el name y la description de cada método tal como llegan: ya vienen
en el idioma de la tienda. Si description es null, el método no tiene
descripción.
En los métodos manuales nadie cobra por ti. Son los que traen isManual: true
en paymentMethods.methods, como bank_transfer, cash_on_delivery o
manual_yape. El pedido queda pendingToConfirm hasta que el comercio confirma
el pago.
En un método manual, action.publicConfiguration trae los datos que configuró
la tienda: phoneNumber, accountHolder y qrImageUrl. Si hay qrImageUrl,
actionKind es qrCode.
{
"paymentMethodCode": "manual_yape",
"providerId": "manual",
"status": "pending",
"actionKind": "qrCode",
"action": {
"publicConfiguration": {
"phoneNumber": "+51987654321",
"accountHolder": "Mi tienda",
"qrImageUrl": "https://…/yape-qr.png"
},
"url": null,
"html": null,
"clientSecret": null,
"qrCode": "https://…/yape-qr.png",
"expiresAt": null
}
}Si la tienda no configuró esos datos, action es null y actionKind es
none. Muestra el name y la description del método.
Tarjeta tokenizada y autenticación 3DS
Con checkoutMode: "embedded", crea el token en el navegador con la clave
pública de publicConfiguration de GET /.
Para Culqi card, carga también https:/, configura su clave
pública y genera el dispositivo con Culqi3DS.generateDevice() antes de pagar.
Envía ese identificador en deviceSessionId: sin él, el pago se rechaza. Culqi
yape usa su propio token y no pasa por 3DS.
{
"paymentMethodCode": "card",
"token": "tkn_live_7Kq2",
"deviceSessionId": "8019959c-fab1-49eb-bbbe-b846d308d8df"
}Si Culqi pide autenticar la tarjeta, el pago llega pending con
actionKind: "threeDS". Configura Culqi3DS.settings con el importe en
céntimos, la moneda, el correo y la URL actual, e inicia
Culqi3DS.initAuthentication(token). Acepta parameters3DS solo de un mensaje
cuyo origin coincida con el de tu página. Envía el resultado con el mismo
token y deviceSessionId, el transactionId del pago y el statusToken:
{
"token": "tkn_live_7Kq2",
"deviceSessionId": "8019959c-fab1-49eb-bbbe-b846d308d8df",
"authentication3DS": {
"eci": "05",
"cavv": "valor-devuelto-por-culqi",
"protocolVersion": "2.2.0",
"directoryServerTransactionId": "id-devuelto-por-culqi"
}
}xid y directoryServerTransactionId son opcionales: envíalos si Culqi los
devuelve. Un desafío vencido no inicia otro cargo, y repetir una autenticación
completada devuelve el mismo resultado.
Con Mercado Pago card, envía también providerPaymentMethodId con el
payment_method_id de la tarjeta; para yape, crea el token con
mp.yape({ phoneNumber, otp }). Un token de otro método se rechaza con
providerCode: "source_mismatch".
Cuotas con tarjeta
Si la tienda ofrece cuotas, cada método con tarjeta de un proveedor que las
acepta (hoy Culqi y Mercado Pago) trae en installments las cantidades que la
tienda acepta para ese total, por ejemplo [2, 3, 4, 5, 6]. Llega vacío si no
las ofrece o si el total no alcanza el monto mínimo. Envía la cantidad elegida en installments; sin ese campo, el
pago va en una sola cuota.
{
"paymentMethodCode": "card",
"token": "tkn_live_7Kq2",
"deviceSessionId": "8019959c-fab1-49eb-bbbe-b846d308d8df",
"installments": 3
}Con Culqi, cada tarjeta acepta sus propias cantidades y las de débito no
aceptan cuotas. Si la tarjeta no acepta la cantidad elegida, el checkout
responde Payments.InstallmentsNotAllowed sin cobrar; pide otra cantidad o el
pago sin cuotas. El interés lo pone el banco del comprador.
Con Mercado Pago, pasa al Card Payment Brick maxInstallments con la mayor
cantidad de installments. Envía en installments e issuerId las cuotas y
el issuer_id que devuelve su onSubmit.
Una cantidad que la tienda no ofrece se rechaza con
Payments.InstallmentsUnavailable.
Pago con código o QR
Culqi plin y pagoefectivo, y Mercado Pago pagoefectivo, generan el código
en el checkout. Envía el método sin token.
{
"paymentMethodCode": "pagoefectivo"
}Muestra voucherCode, qrImageUrl o url de payment.action, con
expiresAt. Culqi Plin vence en 30 minutos y PagoEfectivo, en 24 horas.
El pago queda pending con actionKind: "voucher" hasta que la pasarela
confirma el ingreso. Para volver a mostrar el código, lee pendingPayment en
GET /. Si el checkout no responde a tiempo,
consulta el pedido antes de repetirlo.
Adjunta el comprobante
En un pago manual, el comprador puede subir la foto de su pago. Súbela antes del
checkout con POST /. Luego envía la url
en receiptUrl.
# 1. Sube el comprobante
curl -X POST https://storefront.ecomiq.pe/v1/carts/<cart-token>/payment-receipt \
-H "x-Store-Domain: mi-tienda" \
-F "file=@comprobante-yape.jpg;type=image/jpeg"
# 2. Completa el checkout con su url
curl -X POST https://storefront.ecomiq.pe/v1/carts/<cart-token>/checkout \
-H "x-Store-Domain: mi-tienda" \
-H "Content-Type: application/json" \
-d '{ "paymentMethodCode": "manual_yape", "receiptUrl": "<receipt-url>" }'{
"id": "7204558912004325355",
"url": "https://…/comprobante-yape.jpg"
}El pago del checkout trae esa misma url en receiptUrl. El comercio la ve
junto al pedido.
| Regla | Valor |
|---|---|
| Formatos | JPEG, PNG o WebP. |
| Tamaño máximo | 5 MB. |
| Carrito | Activo. Sube el archivo antes del checkout. |
Si el archivo no cumple, responde 400 con Carts.ReceiptEmpty,
Carts.ReceiptTooLarge, Carts.ReceiptInvalidType o
Carts.ReceiptInvalidContent.
Después del checkout, usa
POST /.
Envía file y el paymentMethodCode de un método manual.
Recibe al comprador
Al terminar en la pasarela, el comprador vuelve a esta URL:
https://<dominio-principal>/orders/<status-token>/payment-returnLa URL siempre usa el dominio principal de la tienda. statusToken identifica
el pedido.
La pasarela puede volver con GET o con POST. Ignora los parámetros y el
cuerpo que traiga. Responde 303 hacia /.
const PAYMENT_RETURN = /^\/orders\/([A-Za-z0-9_-]+)\/payment-return$/;
export function handlePaymentReturn(request) {
const match = PAYMENT_RETURN.exec(new URL(request.url).pathname);
if (match === null) return null;
return new Response(null, {
status: 303,
headers: {
Location: `/orders/${match[1]}`,
"Cache-Control": "private, no-store",
},
});
}Si tu servidor rechaza POST de otro origen, deja pasar esta ruta: solo
redirige. Los correos del pedido también enlazan a /.
El regreso no confirma el pago
La pasarela puede devolver al comprador antes de avisar el resultado. Lee siempre el estado como en Consulta el estado.
Consulta el estado
En /, pide GET /. No
necesita sesión: el token basta.
curl https://storefront.ecomiq.pe/v1/orders/status/<status-token> \
-H "x-Store-Domain: mi-tienda"{
"order": {
"id": "7204558912004325350",
"orderNumber": "ORD-2026-0001",
"orderGroup": "7204558912004325350",
"status": "confirmed",
"total": 170.0,
"totalPaid": 170.0,
"currencyCode": "PEN",
"paymentAt": "2026-10-05T15:34:10Z",
"email": "andrea.ramos@example.com",
"firstName": "Andrea",
"lastName": "Ramos",
"payments": [
{
"id": "7204558912004325364",
"paymentMethod": "powerpay",
"paymentCode": "powerpay",
"amount": 170.0,
"paymentStatus": "captured",
"receiptUrl": null,
"createdOn": "2026-10-05T15:30:02Z"
}
]
},
"step": "confirmed",
"balanceDue": 0.0,
"paymentMethods": []
}step | Qué mostrar |
|---|---|
pending | Pedido recibido. El pago aún no se confirma. |
confirmed | Pago confirmado. |
shipped | Pedido enviado. |
outForDelivery | En reparto. |
delivered | Entregado. |
cancelled | Pedido cancelado. |
Si step es pending, consulta otra vez cada 10 segundos. La pasarela puede
avisar después del regreso. Si balanceDue es mayor que 0, paymentMethods
trae los métodos para pagarlo.
Responde 404 Orders.StatusLinkNotFound si el token no es válido, venció o el
pedido es invalid. El token dura un año. Al volver de la pasarela, trata el
404 como pago fallido: ver Resuelve un pago fallido.
Sin statusToken
En un pago manual el comprador no pasa por la pasarela. Consulta el pedido con el token del carrito:
curl https://storefront.ecomiq.pe/v1/carts/<cart-token>/order \
-H "x-Store-Domain: mi-tienda"{
"orderId": "7204558912004325350",
"orderNumber": "ORD-2026-0001",
"orderGroup": "7204558912004325350",
"orderIds": ["7204558912004325350"],
"status": "pendingToConfirm",
"total": 170.0,
"currencyCode": "PEN",
"createdOn": "2026-10-05T15:30:00Z",
"updatedOn": "2026-10-05T15:30:02Z"
}status es el del primer pedido de orderIds. total suma todos.
status | Qué significa |
|---|---|
new | El pago no terminó de iniciarse. Repite el checkout. |
pendingToConfirm | Espera la confirmación del pago. |
confirmed | Pago confirmado, o pedido sin saldo. |
invalid | El pago falló o se canceló. Ver Resuelve un pago fallido. |
cancelled | El comercio canceló el pedido. |
Si el cliente inició sesión, también sirve
GET /.
Ver Cuentas de clientes.
Resuelve un pago fallido
Si la pasarela avisa que el pago falló o se canceló, el pedido pasa a
invalid. Si no avisa, sigue en pendingToConfirm con saldo.
| Qué pasó | Qué hacer |
|---|---|
| El checkout respondió con error. | El carrito sigue disponible. Repite POST /, con el mismo método u otro. |
| El pago falló después de salir a la pasarela. | Repite el checkout con el mismo cartToken: reabre el carrito, libera sus cupones y crea un pedido nuevo. |
| El comprador no terminó de pagar. | Repite el checkout: devuelve el mismo pago y su action.url. Revisa action.expiresAt. |
Un pago con pasarela que informa action.expiresAt, como PowerPay o
PagoEfectivo, se cancela una hora después de vencer si nadie pagó. El pedido
pasa a invalid y el checkout se puede repetir.
Al repetir el checkout, usa code para decidir:
code | Estado | Qué hacer |
|---|---|---|
Checkout.InProgress | 409 | Otro checkout del carrito sigue en curso. Espera y consulta el pedido. |
Payments.ProviderOperationInProgress | 409 | Ya hay un pago iniciado. Repite con el mismo paymentMethodCode. |
Payments.ProviderOutcomePending | 409 | La pasarela no confirmó el resultado. Consulta el pedido antes de reintentar. |
Checkout.PaymentResolutionPending | 409 | El pago fallido aún se cancela o se devuelve. Reintenta en unos minutos. |
Payments.Rejected | 400 | La pasarela rechazó el pago. params.providerCode trae su motivo. Ofrece otro medio de pago. |
Checkout.NotInProgress | 409 | El carrito ya no está activo. Crea uno nuevo. |
Promotions.CodeNoLongerApplies | 409 | Un código ya no aplica y el carrito lo quitó. Muestra el nuevo total y reintenta. |
Payments.MethodNotAvailable | 400 | El método no está activo. Pide los métodos otra vez con GET /. |
Cobra el saldo
Un pedido con balanceDue mayor que 0 se paga con
POST /. Por ejemplo, un pedido con
bank_transfer que el comprador prefiere pagar con PowerPay. Envía un code
de paymentMethods.
curl -X POST https://storefront.ecomiq.pe/v1/orders/status/<status-token>/payment \
-H "x-Store-Domain: mi-tienda" \
-H "Content-Type: application/json" \
-d '{ "paymentMethodCode": "powerpay" }'{
"actionUrl": "https://pasarela.example.com/pagar/9a7e3f10",
"isCaptured": false,
"transactionId": "7204558912004325370",
"actionKind": "redirect",
"providerId": "powerpay",
"publicConfiguration": {},
"clientSecret": null,
"qrCode": null,
"receiptUrl": null,
"expiresAt": "2026-10-06T15:30:00Z"
}actionKind toma los valores de Continúa el pago. Los datos llegan en el primer nivel:
actionUrl, clientSecret, qrCode, voucherCode y qrImageUrl. El regreso
llega a la misma ruta de Recibe al comprador.
| Respuesta | Cuándo |
|---|---|
409 Orders.NoBalanceDue | El pedido no tiene saldo. |
409 Payments.ProviderOutcomePending | Un pago anterior con pasarela sigue abierto. |
404 Orders.StatusLinkNotFound | El token no es válido o el pedido es invalid o está cancelado. |
Confirma desde tu sistema
El navegador del comprador puede cerrarse antes de volver. Para enterarte en tu backend, suscríbete a estos webhooks de pedidos:
| Evento | Cuándo llega |
|---|---|
order.created | Al crear el pedido, antes de confirmar el pago. Aún no prepares el envío. |
order.confirmed | El pago se confirmó o el comercio confirmó el pedido. |
order.invalidated | El pago falló o se canceló. |
Registra el endpoint con Admin API. Ver Webhooks.
En un pago manual, el comercio confirma el pago en el admin. Desde tu sistema,
captura el pago con POST / y el
transactionId de Lee el pago.
curl -X POST https://api.ecomiq.pe/api/v1/payments/7204558912004325362/capture \
-H "Authorization: Bearer $ECOMIQ_TOKEN"{
"transactionId": "7204558912004325362",
"status": "captured",
"isCaptured": true,
"paymentMethodCode": "manual_yape",
"code": "manual_yape",
"cardType": null,
"amount": 170.0,
"reference": "7204558912004325362",
"providerId": "manual",
"publicConfiguration": {
"phoneNumber": "+51987654321",
"accountHolder": "Mi tienda",
"qrImageUrl": "https://…/yape-qr.png"
},
"clientSecret": null,
"checkoutUrl": null,
"checkoutHtml": null,
"qrCode": "https://…/yape-qr.png",
"receiptUrl": "https://…/comprobante-yape.jpg",
"expiresAt": null,
"actionKind": "qrCode"
}Luego el pedido pasa a confirmed y llega order.confirmed. Capturar un pago
ya captured devuelve el mismo pago. Un pago failed, cancelled o
refunded responde 400 Payments.InvalidStatus.
Siguientes pasos
- Cuentas de clientes: pedidos del cliente con sesión.
- Seguimiento y reseñas: envío y reseñas después de la compra.
- Webhooks: firma, entrega y reintentos.
- Pedidos: estado, saldo y comprobantes en la referencia.
Última actualización: