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"
}
}'| Field | Required | Notes |
|---|---|---|
alias | Yes | A short name for the payment. The buyer sees it on the payment page. |
amount | Yes | A 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. |
currency | Yes | ISO 4217 code. It must be a currency enabled on your account. |
buyer.email | Yes | The buyer's email. Payment Labs reuses the buyer that already has this email on your account, or creates one. |
termsOfServiceUrl, privacyPolicyUrl | Yes | Absolute URLs. |
referenceId, buyer.referenceId | No | Your own identifiers, returned on the checkout so you can match it to your order and customer. |
expiresAt | No | A 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. |
redirectLinks | No | Where the hosted page sends the buyer after the payment. See step 3. |
authenticationOptions | No | ALL (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. |
statementAlias | No | Text 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. |
threeDSecureMethod | No | How 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
sandboxattribute. If your site requires one, include at leastallow-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:
| Outcome | URL used |
|---|---|
| Paid | successRedirectUrl |
| Failed | failureRedirectUrl, with ?reason=<errorReason> and, when known, &extendedReason=<extendedErrorReason> appended |
| Expired | expiredRedirectUrl |
- Opening the link again after the outcome repeats the same redirect.
- Give
failureRedirectUrlwithout 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:
id | Meaning | Extra fields |
|---|---|---|
PL_APP_CHECKOUT_SUCCESS | The checkout is paid. | — |
PL_APP_CHECKOUT_FAILED | The payment did not succeed. | reason: an errorReason value; APPLICATION_ERROR when none is known. |
PL_APP_CHECKOUT_EXPIRED | The 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 byidorreferenceId, 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 exampleshop.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
| Response | Cause | What to do |
|---|---|---|
400 VALIDATION_ERROR | A 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_ALLOWED | currency is not enabled on your account. The body includes currency. | Use an enabled currency, or ask Payment Labs to enable it. |
400 MAXIMUM_AMOUNT_EXCEEDED | amount is above your account limit. The body includes amount and maxAmount. | Lower the amount, or split the payment. |
403 | Your credential lacks the Merchant Write permission. | Use a credential with the right role. |
- Expiry. A checkout past its
expiresAtcan no longer be paid, but its status can stayPENDINGorFAILED. CompareexpiresAtwith 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_FAILEDmessage does not always mean the money did not move. Check the checkout on your server: if its status isUNKNOWN, 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
- Checkout Statuses and Failure Reasons
- Refunds
- Payin SDK, if you want to render the form yourself
Updated 7 days ago
What’s Next
Next: Payin SDK.
