Hosted Checkout Page and Iframe

Create a checkout on your server, let the buyer pay on the hosted page or in an iframe, and confirm the payment before you fulfil the order.

This guide takes a single payment from a buyer with the page Payment Labs hosts for you, either as a link or inside an iframe on your site. To render the payment form with your own JavaScript instead, use the Payin SDK.

Before you start

  • Read the Payins Overview for the objects and the overall flow.
  • Get a token with Create Access Token, the same login payors use, from a credential with the Merchant Write permission. See Permissions.
  • Have the URLs of your terms of service and privacy policy. The buyer sees links to both on the payment page.

Step 1: Create a checkout

Call Create Checkout from your server.

curl -X POST https://api-sandbox.paymentlabs.io/checkouts \
  -H "Authorization: Bearer $PAYMENT_LABS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "Order 10482",
    "referenceId": "order-10482",
    "description": "Annual membership",
    "amount": 49.99,
    "currency": "USD",
    "buyer": { "email": "[email protected]", "referenceId": "customer-7731" },
    "termsOfServiceUrl": "https://shop.example.com/terms",
    "privacyPolicyUrl": "https://shop.example.com/privacy",
    "expiresAt": "2026-11-30",
    "redirectLinks": {
      "successRedirectUrl": "https://shop.example.com/orders/10482/paid",
      "failureRedirectUrl": "https://shop.example.com/orders/10482/failed",
      "expiredRedirectUrl": "https://shop.example.com/orders/10482/expired"
    }
  }'
FieldRequiredNotes
aliasYesA short name for the payment. The buyer sees it on the payment page.
amountYesA decimal number in currency, for example 49.99. Zero creates a checkout that saves the buyer's card or bank account without charging it; the buyer must sign in. See Saved Cards and Merchant-Initiated Charges.
currencyYesISO 4217 code. It must be a currency enabled on your account.
buyer.emailYesThe buyer's email. Payment Labs reuses the buyer that already has this email on your account, or creates one.
termsOfServiceUrl, privacyPolicyUrlYesAbsolute URLs.
referenceId, buyer.referenceIdNoYour own identifiers, returned on the checkout so you can match it to your order and customer.
expiresAtNoA date (2026-11-30) or date-time. The buyer can pay until 23:59:59 of that day in userTimezone (an IANA name such as America/New_York; UTC when omitted). Defaults to one year after you create the checkout.
redirectLinksNoWhere the hosted page sends the buyer after the payment. See step 3.
authenticationOptionsNoALL (default) lets the buyer pay as a guest or sign in. GUEST_ONLY hides sign-in. AUTHENTICATED_ONLY requires the buyer to sign in, which they need to save a card for later.
statementAliasNoText shown on the buyer's card statement after your merchant prefix, as PREFIX*STATEMENT ALIAS. Accepted only when soft descriptors are enabled for your account: letters, digits, spaces and ., , or -, and at most 22 characters for the whole descriptor, prefix and * included.
threeDSecureMethodNoHow strictly 3D Secure is enforced for card payments: REQUIRE_3DS, PREFER_3DS or SKIP_3DS. Omit it to use your account's default. A method not enabled for your account returns 400 VALIDATION_ERROR.

The request body is strict: an unknown field returns 400. The response is the new checkout, with status PENDING:

{
  "id": "S-01JBX7Z3K9M2N4P6Q8R0S2T4V6",
  "merchantId": "C-01JBX2Q8F4H6K8M0N2P4R6T8VW",
  "merchantName": "Example Shop",
  "buyerEmail": "[email protected]",
  "alias": "Order 10482",
  "referenceId": "order-10482",
  "amount": { "value": 49.99, "currency": "USD", "formattedValue": "$49.99", "digits": 2, "wholeValue": 4999 },
  "payableAmount": { "value": 49.99, "currency": "USD", "formattedValue": "$49.99", "digits": 2, "wholeValue": 4999 },
  "status": "PENDING",
  "expiresAt": "2026-11-30T23:59:59.999Z",
  "authenticationOptions": "ALL",
  "links": {
    "checkout": "https://app-sandbox.paymentlabs.io/checkout/attempt/S-01JBX7Z3K9M2N4P6Q8R0S2T4V6",
    "embeddable": "https://checkout-sandbox.paymentlabs.io/checkout/attempt/S-01JBX7Z3K9M2N4P6Q8R0S2T4V6?config=eyJjaGVja291dElkIjoiUy0wMUpCWDdaM0s5TTJONFA2UThSMFMyVDRWNiJ9"
  }
}

Store id with your order. You need it to confirm the payment and to refund it.

Step 2: Show the payment page

Pick one of the two links on the checkout.

Hosted link

Send the buyer to links.checkout: redirect them, put it behind a Pay button, or email it. The page works on desktop and mobile browsers.

Iframe

Load links.embeddable in an iframe on your page. Use the URL exactly as returned; it carries a signed configuration.

<iframe
  id="payment-labs-checkout"
  src="EMBEDDABLE_URL_FROM_THE_CHECKOUT"
  allow="payment"
  style="width: 100%; min-height: 640px; border: 0"
></iframe>
  • allow="payment" is required for Apple Pay and Google Pay inside the frame.
  • Leave out the sandbox attribute. If your site requires one, include at least allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox; without the first three the form cannot load or submit.

Step 3: Handle the result in the browser

Hosted link: redirects

When the buyer finishes, the hosted page shows a result message and, if you set redirectLinks, sends the browser to the matching URL:

OutcomeURL used
PaidsuccessRedirectUrl
FailedfailureRedirectUrl, with ?reason=<errorReason> and, when known, &extendedReason=<extendedErrorReason> appended
ExpiredexpiredRedirectUrl
  • Opening the link again after the outcome repeats the same redirect.
  • Give failureRedirectUrl without a query string of its own, because the page appends ?reason=… directly.
  • Without redirectLinks, the buyer stays on the result message.
  • If the charge cannot be confirmed (checkout status UNKNOWN), the page asks the buyer not to pay again and does not redirect.

Iframe: messages

The embedded page posts a message to your page when the payment ends. Each message is an object with an id, and failures add a reason:

idMeaningExtra fields
PL_APP_CHECKOUT_SUCCESSThe checkout is paid.—
PL_APP_CHECKOUT_FAILEDThe payment did not succeed.reason: an errorReason value; APPLICATION_ERROR when none is known.
PL_APP_CHECKOUT_EXPIREDThe checkout expired before it was paid.—

The frame posts the same message again if the buyer reloads a finished checkout. redirectLinks also apply inside the iframe and load in the frame itself, so leave them out for embedded checkouts and act on the messages.

const PAYMENT_LABS_ORIGINS = [
  "https://checkout.paymentlabs.io",          // production
  "https://checkout-sandbox.paymentlabs.io",  // sandbox
];

window.addEventListener("message", (event) => {
  if (!PAYMENT_LABS_ORIGINS.includes(event.origin)) return;

  const { id, reason } = event.data ?? {};
  const frame = document.getElementById("payment-labs-checkout");

  if (id === "PL_APP_CHECKOUT_SUCCESS") {
    frame.style.display = "none";
    showMessage("Thank you. We are confirming your payment.");
  } else if (id === "PL_APP_CHECKOUT_FAILED") {
    showMessage(`The payment did not go through (${reason}).`);
  } else if (id === "PL_APP_CHECKOUT_EXPIRED") {
    showMessage("This payment link has expired.");
  }
});

Use these messages to update your page only. Confirm the payment on your server before you fulfil the order.

Step 4: Confirm the payment on your server

Fulfil the order only after your server sees status: "PAID". Use either method:

  • Webhook. Register checkout-status-changed (see Webhooks). Each delivery carries the checkout as it is when the event is sent. Match it to your order by id or referenceId, and make your handler safe to run twice for the same checkout.
  • Polling. Call Get Checkout when the buyer returns to your site, and every few seconds while the status is PROCESSING.
curl https://api-sandbox.paymentlabs.io/checkouts/S-01JBX7Z3K9M2N4P6Q8R0S2T4V6 \
  -H "Authorization: Bearer $PAYMENT_LABS_TOKEN"

Once the checkout is PAID, chargedAmount holds the amount charged and succeededTransactionId holds the transaction to refund if you need to. To find checkouts in bulk, use Search Checkouts.

Payment methods

The page offers the payment methods enabled on your account for the checkout's currency and amount: cards, bank transfers, Apple Pay and Google Pay. A buyer can pay as a guest, or sign in to reuse a saved card and to save a new one. authenticationOptions controls which of the two they see.

Apple Pay and Google Pay

  • Ask Payment Labs to enable them on your account.
  • Apple Pay in an iframe needs allow="payment" on the iframe and Safari on iOS 17 or later. Payment Labs must also register the domain of the page that hosts the iframe, for example shop.example.com. Send that domain to your Payment Labs contact before you go live. Without it, the Apple Pay button does not appear.
  • On the hosted link no extra setup is needed.
  • If the buyer's device or browser does not support a wallet, the page does not offer it. The other methods still work.

Errors and edge cases

ResponseCauseWhat to do
400 VALIDATION_ERRORA field is missing, malformed or unknown. details names each field. An expiresAt that has already passed is reported as details.expiresAt.Fix the field and send the request again.
400 CURRENCY_NOT_ALLOWEDcurrency is not enabled on your account. The body includes currency.Use an enabled currency, or ask Payment Labs to enable it.
400 MAXIMUM_AMOUNT_EXCEEDEDamount is above your account limit. The body includes amount and maxAmount.Lower the amount, or split the payment.
403Your credential lacks the Merchant Write permission.Use a credential with the right role.
  • Expiry. A checkout past its expiresAt can no longer be paid, but its status can stay PENDING or FAILED. Compare expiresAt with the current time instead of relying on the status alone.
  • Failed payments. After a failure the hosted page and the iframe show the result and do not offer another attempt. To let the buyer try again, create a new checkout for the same order and send them the new link. See Checkout Statuses and Failure Reasons.
  • Unconfirmed payments. A PL_APP_CHECKOUT_FAILED message does not always mean the money did not move. Check the checkout on your server: if its status is UNKNOWN, do not ask the buyer to pay again and do not cancel the order until it is resolved.

Test it in sandbox

Pay the sandbox checkout with the cards on Credit Card Payin Test Values, and use the address values on that page to force a failure.

Next steps


What’s Next

Next: Payin SDK.

Did this page help you?