Skip to content
Online store

Checkout and payment

Send the buyer to pay, bring them back, and confirm the payment with the Storefront API.

Andrea confirms her cart and chooses to pay with PowerPay. The checkout creates the order and starts the payment; your store sends her to pay, brings her back, and checks whether she paid.

Buyer
Your store
Ecomiq
Provider
POST /v1/carts/{cartToken}/checkout
payment with action.url
Redirect to action.url
Pays
Payment result
Back to /orders/{statusToken}/payment-return
GET /v1/orders/status/{statusToken}
step and balanceDue
The result reaches Ecomiq; the return only brings the buyer back.

How payment works

The payment method decides what your store does:

  • Redirect. The buyer pays on the provider's page and comes back to your store.
  • Form. You mount the provider's form in your checkout.
  • Manual. Bank transfer, Yape, or cash on delivery. You show the instructions and the merchant confirms the payment.

In all three cases, the provider or the merchant confirms the payment. Your store checks the status.

Requirements

  • A cart ready for checkout. See Headless store.
  • Your frontend on HTTPS at the store's primary domain. GET /v1/stores/domains marks it with isPrimary: true.
  • The /orders/{statusToken} and /orders/{statusToken}/payment-return routes on that domain.
  • For Confirm from your system, an Admin token with the payment:create permission in ECOMIQ_TOKEN. See Quickstart.
  • Node.js or Python with requests, for the examples.

Read the payment

POST /v1/carts/{cartToken}/checkout creates the order and starts the payment. The response returns the payment in payment. This example pays with PowerPay, which charges outside your store.

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" }'
Response (excerpt)
{
  "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"
    }
  }
}

The response also returns cart, already completed, and orderStatusUrl, the order status page, which ends in /orders/{statusToken}. Store orderId, orderNumber, the cartToken, and the statusToken: you use them to check the status. The next steps reuse API and headers.

FieldContains
transactionIdPayment ID. Use it to confirm the payment from your system.
paymentMethodCodeMethod chosen at checkout.
providerIdProvider that charges. manual when there is no provider.
statusPayment status.
amountAmount to charge: the total of every order in the cart.
referenceThe provider's reference, your externalRef, or the transactionId, in that order.
actionKindWhat the buyer does to pay. See Continue the payment.
receiptUrlAttached receipt, if you sent one.
actionData to continue the payment. null when nothing else is needed.
statusMeaning
pendingThe buyer has not paid, or the merchant has not confirmed the payment.
authorizedThe provider reserved the amount. It still needs a capture.
capturedPayment collected. The order is confirmed.
failedThe provider rejected the payment.
cancelledThe payment was cancelled.
refundedThe payment was refunded.

If the total is 0, payment is null and the order is confirmed. While the order stays active, repeating the checkout for the same cart returns the same order and payment.

Continue the payment

actionKind tells you what to do with the buyer. action carries the data.

actionKindWhat to do
redirectRedirect the buyer to action.url. If action.expiresAt is set, the link expires then.
embeddedMount the provider's form on your page with action.clientSecret or action.html, plus the public keys in action.publicConfiguration.
qrCodeShow the QR in action.qrCode. With providerId: "manual", it is an image URL.
voucherShow the code in action.voucherCode and the QR image in action.qrImageUrl. The buyer pays outside the checkout.
threeDSAuthenticate the card. See Tokenized card and 3DS authentication.
noneIf status is captured, show the confirmation. Otherwise show the method's instructions: the merchant confirms the payment.
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, and showInstructions are your store's functions. In Python, /checkout/pay shows the form, the QR, or the instructions.

Show each method's name and description as they arrive: they are already in the store's language. If description is null, the method has no description.

With manual methods, nobody charges for you. They are the ones with isManual: true in paymentMethods.methods, such as bank_transfer, cash_on_delivery, or manual_yape. The order stays pendingToConfirm until the merchant confirms the payment.

For a manual method, action.publicConfiguration carries the data the store set up: phoneNumber, accountHolder, and qrImageUrl. If there is a qrImageUrl, actionKind is qrCode.

payment with manual_yape (excerpt)
{
  "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
  }
}

If the store did not set up that data, action is null and actionKind is none. Show the method's name and description.

Tokenized card and 3DS authentication

With checkoutMode: "embedded", create the token in the browser using the public key in publicConfiguration from GET /v1/carts/{cartToken}/checkout. For Culqi card, also load https://3ds.culqi.com, configure its public key, and call Culqi3DS.generateDevice() before paying. Send that identifier in deviceSessionId: without it, the payment is rejected. Culqi yape uses its own token and skips 3DS.

Body of POST /v1/carts/{cartToken}/checkout
{
  "paymentMethodCode": "card",
  "token": "tkn_live_7Kq2",
  "deviceSessionId": "8019959c-fab1-49eb-bbbe-b846d308d8df"
}

If Culqi asks to authenticate the card, the payment arrives pending with actionKind: "threeDS". Configure Culqi3DS.settings with the amount in minor units, the currency, the email, and the current URL, then call Culqi3DS.initAuthentication(token). Accept parameters3DS only from a message whose origin matches your page. Send the result with the same token and deviceSessionId, the payment's transactionId, and the statusToken:

Body of POST /v1/orders/status/{statusToken}/payments/{transactionId}/authenticate
{
  "token": "tkn_live_7Kq2",
  "deviceSessionId": "8019959c-fab1-49eb-bbbe-b846d308d8df",
  "authentication3DS": {
    "eci": "05",
    "cavv": "value-returned-by-culqi",
    "protocolVersion": "2.2.0",
    "directoryServerTransactionId": "id-returned-by-culqi"
  }
}

xid and directoryServerTransactionId are optional: include them when Culqi returns them. An expired challenge doesn't start another charge, and repeating a completed authentication returns the same result.

For Mercado Pago card, also send the card's payment_method_id in providerPaymentMethodId; for yape, create the token with mp.yape({ phoneNumber, otp }). A token from another method is rejected with providerCode: "source_mismatch".

Card installments

When the store offers installments, each card method of a provider that accepts them (Culqi and Mercado Pago today) lists in installments the counts the store accepts for that total, for example [2, 3, 4, 5, 6]. It is empty when the store doesn't offer them or the total is below the minimum amount. Send the chosen count in installments; without it, the payment goes in a single installment.

POST /v1/carts/{cartToken}/checkout body
{
  "paymentMethodCode": "card",
  "token": "tkn_live_7Kq2",
  "deviceSessionId": "8019959c-fab1-49eb-bbbe-b846d308d8df",
  "installments": 3
}

With Culqi, each card accepts its own counts and debit cards accept none. If the card doesn't accept the chosen count, checkout answers Payments.InstallmentsNotAllowed without charging; ask for another count or a single payment. The buyer's bank sets the interest.

With Mercado Pago, pass the largest count in installments to the Card Payment Brick as maxInstallments. Send the installments and the issuer_id its onSubmit returns in installments and issuerId.

A count the store doesn't offer is rejected with Payments.InstallmentsUnavailable.

Payment with a code or QR

Culqi plin and pagoefectivo, and Mercado Pago pagoefectivo, generate the code during checkout. Send the method without a token.

Body of POST /v1/carts/{cartToken}/checkout
{
  "paymentMethodCode": "pagoefectivo"
}

Show voucherCode, qrImageUrl, or url from payment.action, with expiresAt. Culqi Plin expires in 30 minutes and PagoEfectivo in 24 hours.

The payment stays pending with actionKind: "voucher" until the provider confirms it. To show the code again, read pendingPayment from GET /v1/orders/status/{statusToken}. If the checkout times out, check the order before repeating it.

Attach the receipt

With a manual payment, the buyer can upload a photo of their payment. Upload it before checkout with POST /v1/carts/{cartToken}/payment-receipt. Then send the url in receiptUrl.

# 1. Upload the receipt
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. Complete the checkout with its 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>" }'
Receipt response
{
  "id": "7204558912004325355",
  "url": "https://…/comprobante-yape.jpg"
}

The checkout payment returns that same url in receiptUrl. The merchant sees it with the order.

RuleValue
FormatsJPEG, PNG, or WebP.
Maximum size5 MB.
CartActive. Upload the file before checkout.

If the file does not comply, the API responds 400 with Carts.ReceiptEmpty, Carts.ReceiptTooLarge, Carts.ReceiptInvalidType, or Carts.ReceiptInvalidContent.

After checkout, use POST /v1/orders/status/{statusToken}/payment-receipt. Send file and the paymentMethodCode of a manual method.

Receive the buyer

When they finish with the provider, the buyer comes back to this URL:

https://<primary-domain>/orders/<status-token>/payment-return

The URL always uses the store's primary domain. statusToken identifies the order.

The provider may return with GET or POST. Ignore any parameters or body it sends. Respond 303 to /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",
    },
  });
}

If your server rejects cross-origin POST requests, let this route through: it only redirects. Order emails also link to /orders/{statusToken}.

The return does not confirm the payment

The provider may send the buyer back before it reports the result. Always read the status as in Check the status.

Check the status

On /orders/{statusToken}, call GET /v1/orders/status/{statusToken}. It needs no session: the token is enough.

curl https://storefront.ecomiq.pe/v1/orders/status/<status-token> \
  -H "x-Store-Domain: mi-tienda"
Response (excerpt)
{
  "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": []
}
stepWhat to show
pendingOrder received. The payment is not confirmed yet.
confirmedPayment confirmed.
shippedOrder shipped.
outForDeliveryOut for delivery.
deliveredDelivered.
cancelledOrder cancelled.

If step is pending, check again every 10 seconds. The provider may report after the buyer returns. If balanceDue is greater than 0, paymentMethods lists the methods to pay it.

The API responds 404 Orders.StatusLinkNotFound when the token is invalid or expired, or the order is invalid. The token lasts one year. Right after the provider return, treat a 404 as a failed payment: see Handle a failed payment.

Without a statusToken

With a manual payment, the buyer never goes through a provider. Check the order with the cart token:

curl https://storefront.ecomiq.pe/v1/carts/<cart-token>/order \
  -H "x-Store-Domain: mi-tienda"
Response
{
  "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 belongs to the first order in orderIds. total adds them all.

statusMeaning
newThe payment did not finish starting. Repeat the checkout.
pendingToConfirmWaiting for payment confirmation.
confirmedPayment confirmed, or the order had nothing to pay.
invalidThe payment failed or was cancelled. See Handle a failed payment.
cancelledThe merchant cancelled the order.

If the customer is signed in, you can also use GET /v1/customer/orders/{orderId}/status. See Customer accounts.

Handle a failed payment

If the provider reports that the payment failed or was cancelled, the order becomes invalid. If it reports nothing, the order stays pendingToConfirm with a balance due.

What happenedWhat to do
The checkout returned an error.The cart is still available. Repeat POST /v1/carts/{cartToken}/checkout, with the same method or another one.
The payment failed after leaving for the provider.Repeat the checkout with the same cartToken: it reopens the cart, releases its coupons, and creates a new order.
The buyer did not finish paying.Repeat the checkout: it returns the same payment and its action.url. Check action.expiresAt.

A provider payment that reports action.expiresAt, such as PowerPay or PagoEfectivo, is cancelled one hour after it expires if nobody paid. The order becomes invalid and the checkout can be repeated.

When you repeat the checkout, use code to decide:

codeStatusWhat to do
Checkout.InProgress409Another checkout for the cart is still running. Wait and check the order.
Payments.ProviderOperationInProgress409A payment has already started. Repeat with the same paymentMethodCode.
Payments.ProviderOutcomePending409The provider has not confirmed the outcome. Check the order before retrying.
Checkout.PaymentResolutionPending409The failed payment is still being cancelled or refunded. Retry in a few minutes.
Payments.Rejected400The provider declined the payment. params.providerCode carries its reason. Offer another payment method.
Checkout.NotInProgress409The cart is no longer active. Create a new one.
Promotions.CodeNoLongerApplies409A code no longer applies and the cart dropped it. Show the new total and retry.
Payments.MethodNotAvailable400The method is not active. Fetch the methods again with GET /v1/carts/{cartToken}/checkout.

Pay the balance

An order with balanceDue greater than 0 is paid with POST /v1/orders/status/{statusToken}/payment. For example, a bank_transfer order that the buyer would rather pay with PowerPay. Send a code from 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" }'
Response
{
  "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 takes the values from Continue the payment. The data comes at the top level: actionUrl, clientSecret, qrCode, voucherCode, and qrImageUrl. The buyer returns to the same route as in Receive the buyer.

ResponseWhen
409 Orders.NoBalanceDueThe order has no balance due.
409 Payments.ProviderOutcomePendingAn earlier provider payment is still open.
404 Orders.StatusLinkNotFoundThe token is invalid, or the order is invalid or cancelled.

Confirm from your system

The buyer may close the browser before coming back. To hear about the payment in your backend, subscribe to these order webhooks:

EventWhen it arrives
order.createdWhen the order is created, before the payment is confirmed. Hold the shipment for now.
order.confirmedThe payment was confirmed, or the merchant confirmed the order.
order.invalidatedThe payment failed or was cancelled.

Register the endpoint with the Admin API. See Webhooks.

With a manual payment, the merchant confirms it in the admin. From your system, capture the payment with POST /v1/payments/{paymentTransactionId}/capture and the transactionId from Read the payment.

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

The order then becomes confirmed and order.confirmed arrives. Capturing a payment that is already captured returns the same payment. A failed, cancelled, or refunded payment returns 400 Payments.InvalidStatus.

Next steps

Last updated:

On this page