Saltar al contenido
Tienda online

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ó.

Comprador
Tu tienda
Ecomiq
Pasarela
POST /v1/carts/{cartToken}/checkout
payment con action.url
Redirige a action.url
Paga
Resultado del pago
Vuelve a /orders/{statusToken}/payment-return
GET /v1/orders/status/{statusToken}
step y balanceDue
El resultado llega a Ecomiq; el regreso solo trae al comprador.

Có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 /v1/stores/domains lo marca con isPrimary: true.
  • Las rutas /orders/{statusToken} y /orders/{statusToken}/payment-return en ese dominio.
  • Para confirmar desde tu sistema, un token de Admin con el permiso payment:create en ECOMIQ_TOKEN. Ver Inicio rápido.
  • Node.js o Python con requests, para los ejemplos.

Lee el pago

POST /v1/carts/{cartToken}/checkout 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" }'
Respuesta (extracto)
{
  "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 /orders/{statusToken}. Guarda orderId, orderNumber, el cartToken y el statusToken: los usas para consultar el estado. Los pasos siguientes reutilizan API y headers.

CampoQué lleva
transactionIdID del pago. Lo usas para confirmarlo desde tu sistema.
paymentMethodCodeMétodo elegido en el checkout.
providerIdPasarela que cobra. manual si no hay pasarela.
statusEstado del pago.
amountMonto a cobrar: el total de todos los pedidos del carrito.
referenceReferencia de la pasarela, tu externalRef o el transactionId, en ese orden.
actionKindQué hace el comprador para pagar. Ver Continúa el pago.
receiptUrlComprobante adjunto, si lo enviaste.
actionDatos para continuar el pago. null si no hace falta nada.
statusQué significa
pendingFalta que el comprador pague o que el comercio confirme el pago.
authorizedLa pasarela reservó el monto. Falta capturarlo.
capturedPago cobrado. El pedido queda confirmed.
failedLa pasarela rechazó el pago.
cancelledEl pago se canceló.
refundedEl 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.

actionKindQué hacer
redirectRedirige al comprador a action.url. Si viene action.expiresAt, el enlace vence entonces.
embeddedMonta el formulario de la pasarela en tu página con action.clientSecret o action.html, y las claves públicas de action.publicConfiguration.
qrCodeMuestra el QR de action.qrCode. Con providerId: "manual", es la URL de una imagen.
voucherMuestra el código de action.voucherCode y la imagen del QR de action.qrImageUrl. El comprador paga fuera del checkout.
threeDSAutentica la tarjeta. Ver Tarjeta tokenizada y autenticación 3DS.
noneSi 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, /checkout/pay 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.

payment con manual_yape (extracto)
{
  "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 /v1/carts/{cartToken}/checkout. Para Culqi card, carga también https://3ds.culqi.com, 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.

Cuerpo de POST /v1/carts/{cartToken}/checkout
{
  "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:

Cuerpo de POST /v1/orders/status/{statusToken}/payments/{transactionId}/authenticate
{
  "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.

Cuerpo de POST /v1/carts/{cartToken}/checkout
{
  "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.

Cuerpo de POST /v1/carts/{cartToken}/checkout
{
  "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 /v1/orders/status/{statusToken}. 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 /v1/carts/{cartToken}/payment-receipt. 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>" }'
Respuesta del comprobante
{
  "id": "7204558912004325355",
  "url": "https://…/comprobante-yape.jpg"
}

El pago del checkout trae esa misma url en receiptUrl. El comercio la ve junto al pedido.

ReglaValor
FormatosJPEG, PNG o WebP.
Tamaño máximo5 MB.
CarritoActivo. 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 /v1/orders/status/{statusToken}/payment-receipt. 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-return

La 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 /orders/{statusToken}.

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 /orders/{statusToken}.

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 /orders/{statusToken}, pide GET /v1/orders/status/{statusToken}. No necesita sesión: el token basta.

curl https://storefront.ecomiq.pe/v1/orders/status/<status-token> \
  -H "x-Store-Domain: mi-tienda"
Respuesta (extracto)
{
  "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": []
}
stepQué mostrar
pendingPedido recibido. El pago aún no se confirma.
confirmedPago confirmado.
shippedPedido enviado.
outForDeliveryEn reparto.
deliveredEntregado.
cancelledPedido 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"
Respuesta
{
  "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.

statusQué significa
newEl pago no terminó de iniciarse. Repite el checkout.
pendingToConfirmEspera la confirmación del pago.
confirmedPago confirmado, o pedido sin saldo.
invalidEl pago falló o se canceló. Ver Resuelve un pago fallido.
cancelledEl comercio canceló el pedido.

Si el cliente inició sesión, también sirve GET /v1/customer/orders/{orderId}/status. 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 /v1/carts/{cartToken}/checkout, 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:

codeEstadoQué hacer
Checkout.InProgress409Otro checkout del carrito sigue en curso. Espera y consulta el pedido.
Payments.ProviderOperationInProgress409Ya hay un pago iniciado. Repite con el mismo paymentMethodCode.
Payments.ProviderOutcomePending409La pasarela no confirmó el resultado. Consulta el pedido antes de reintentar.
Checkout.PaymentResolutionPending409El pago fallido aún se cancela o se devuelve. Reintenta en unos minutos.
Payments.Rejected400La pasarela rechazó el pago. params.providerCode trae su motivo. Ofrece otro medio de pago.
Checkout.NotInProgress409El carrito ya no está activo. Crea uno nuevo.
Promotions.CodeNoLongerApplies409Un código ya no aplica y el carrito lo quitó. Muestra el nuevo total y reintenta.
Payments.MethodNotAvailable400El método no está activo. Pide los métodos otra vez con GET /v1/carts/{cartToken}/checkout.

Cobra el saldo

Un pedido con balanceDue mayor que 0 se paga con POST /v1/orders/status/{statusToken}/payment. 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" }'
Respuesta
{
  "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.

RespuestaCuándo
409 Orders.NoBalanceDueEl pedido no tiene saldo.
409 Payments.ProviderOutcomePendingUn pago anterior con pasarela sigue abierto.
404 Orders.StatusLinkNotFoundEl 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:

EventoCuándo llega
order.createdAl crear el pedido, antes de confirmar el pago. Aún no prepares el envío.
order.confirmedEl pago se confirmó o el comercio confirmó el pedido.
order.invalidatedEl 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 /v1/payments/{paymentTransactionId}/capture y el transactionId de Lee el pago.

curl -X POST https://api.ecomiq.pe/api/v1/payments/7204558912004325362/capture \
  -H "Authorization: Bearer $ECOMIQ_TOKEN"
Respuesta
{
  "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

Última actualización:

En esta página