Embedding Checkout
Embedded Checkout
Embedded Checkout lets your customers pay without leaving your site. You put the Advance checkout inside an iframe on your page, and it tells your page when the payment succeeds or fails.
How It Works
- Your server creates a Plan or a Payment Request with the Advance API and gets back a checkout URL.
- Your page shows that URL in an iframe, with
?embed=trueadded. - Your customer pays inside the iframe (credit card or bank account).
- Your page receives a message (
success,error, thensettled) and moves on, for example to a confirmation page.
With ?embed=true, the checkout hides the Advance tenant logo, fits neatly inside an iframe, and sends payment updates to your page through window.postMessage. If your page already shows the invoice, add &fields_only=true to show only the payment form (see Layouts). You can also restyle it to match your site. See Styling Your Embed.
Want to see it first? Open the Checkout Embed Playground, paste a checkout URL, and watch the events it sends.
Quick Start
1. Get a checkout URL (server-side)
Create either a Plan (invoice-based) or a standalone Payment Request with the Advance API. Always do this from your backend, because it needs your API key. Use allowed_payment_methods to choose which payment methods the checkout offers.
A Plan returns its checkout URL as payment_url; a Payment Request returns it as payment_request_url. See the API Reference for the full request and response.
2. Put it in an iframe
Add ?embed=true to the URL:
<iframe
id="advance-checkout"
src="https://app.advancehq.com/checkout/pay_xyz789?embed=true"
style="width: 100%; height: 700px; border: none;"
allow="payment"
></iframe>If your page already shows the invoice, embed only the payment form:
<iframe
id="advance-checkout"
src="https://app.advancehq.com/checkout/pay_xyz789?embed=true&fields_only=true"
style="width: 100%; height: 600px; border: none;"
allow="payment"
></iframe>3. Listen for the result
window.addEventListener('message', event => {
if (event.origin !== 'https://app.advancehq.com') return; // only trust Advance
const {type, payload} = event.data;
switch (type) {
case 'advance.checkout.ready': // the checkout has loaded
break;
case 'advance.checkout.success':
// payload: { payment_id, amount, payment_method, status }, or { payment_id, already_paid: true }
// status 'paid' = card payment received; 'processing' = bank debit authorized, not yet settled
break;
case 'advance.checkout.error': // payload: { error_code, error_message }; the customer can retry
break;
case 'advance.checkout.settled': // no payload; always fires after success or error: clean up here
break;
}
});That's all you need to take payments inside your app.
Layouts
Each embed chooses its own layout through the URL. There is no dashboard setting.
| Layout | URL | What the customer sees |
|---|---|---|
| Full (default) | ?embed=true | The invoice summary card (invoice number, due date, billed to, amount due, invoice link) above the payment card. |
| Payment fields only | ?embed=true&fields_only=true | Only the Payment Details card: the Credit Card / Bank Account toggle, the payment fields, the terms line, the fee breakdown and the pay button. |
Notes on the payment-fields-only layout:
- The pay button always shows the total, including any processing fee (for example Pay $1,287.50 Now), so the customer still sees what they will be charged.
- The Credit Card / Bank Account toggle is hidden when the plan allows only one method.
- If the plan offers premium financing, the Pay in full / Monthly payments selector stays at the top of the payment card, since the customer chooses there how to pay.
- The card fills the width of your iframe up to 800px, and is centered beyond that. It works down to phone width, with no horizontal scrolling.
- Like
style,fields_onlyonly works inside an iframe withembed=true. The same link opened in its own tab (for example from the invoice email) shows the full checkout. fields_onlyandstylecan be combined.
JavaScript SDK (Not available yet)
Not available yet. There is no JavaScript SDK for embedded checkout today:
https://js.advancehq.com/checkout.jsandAdvanceCheckout.createdo not exist. Embed the iframe and listen formessageevents as shown in the Quick Start.
Styling Your Embed
You can make the checkout match your brand. You can change:
- Colors: text, backgrounds, the pay button, tabs and links
- Fonts: choose from 10 popular fonts, plus the weight and size
- Corner radius: of the pay button and the cards
Spacing and layout always stay as designed. Anything you don't set keeps the standard Advance look.
Style in 3 steps
// 1. Describe your look
const style = {
variables: {colorPrimary: '#5B2EFF', fontFamily: 'Poppins'},
};
// 2. Encode it
const encoded = encodeURIComponent(JSON.stringify(style));
// 3. Add it to the iframe URL, next to embed=true
iframe.src = `${checkoutUrl}?embed=true&style=${encoded}`;A style has three keys, and all of them are optional:
| Key | What it does |
|---|---|
variables | Your defaults for the whole checkout, e.g. "my brand color is purple" |
rules | Changes for one specific part, e.g. "make only the pay button round". A rule wins over a variable. |
showPoweredBy | false hides "· Powered by Advance" under the pay button |
What you can change
Each row below is one part of the checkout. The middle column shows the variable that changes it across the whole checkout. The right column shows the rule that changes only that part.
| Part | Change it everywhere (variables) | Change only this part (rules) |
|---|---|---|
| Cards | colorBackground, borderRadius | .Card (both cards), .SummaryCard, .PaymentCard |
| Labels ("Billed To", "Due Date") | colorTextSecondary | .Label |
Values (name, date; the email is .ValueSecondary) | colorText | .Value, .ValueSecondary |
| Amount due | colorText | .Amount |
| Links (invoice download, policies) | colorPrimary | .Link |
| Heading | colorText | .Heading |
| Subheading | colorTextSecondary | .Subheading |
| Credit Card / Bank Account tabs | colorPrimary (selected), colorSurface (track), colorTextSecondary (unselected) | .TabList, .Tab, .Tab--selected |
| Policy / ACH disclaimer | colorText | .Disclaimer |
| Pay button | colorPrimary, colorPrimaryText | .PayButton, .PayButton:hover, .PayButton--disabled |
| "Secured with 256-bit encryption · Powered by Advance" | colorTextMuted | .SecureNote, .PoweredBy (or hide it with showPoweredBy: false) |
| Area behind the cards | colorPageBackground | (variable only) |
| Card and bank input fields | Can't be changed. The payment processor draws them. |
fontFamily in variables changes the font everywhere. In a rule, it changes only that part.
Some parts only show up in certain flows: payment plans (.PlanCard, .PlanTitle, …), financing details (.Accordion), the fee breakdown (.FeeRow, .TotalAmount, …), the financing terms checkbox (.Checkbox) and the success screen (.SuccessTitle, …). They're all listed in the reference below.
Common recipes
| I want to… | Add this to style |
|---|---|
| Use my brand color for the button, tabs and links | variables: {colorPrimary: '#5B2EFF'} |
| Make only the pay button a different color | rules: {'.PayButton': {backgroundColor: '#0F766E'}} |
| Darken the button on hover | rules: {'.PayButton:hover': {backgroundColor: '#4A22D9'}} |
| Make the pay button pill-shaped | rules: {'.PayButton': {borderRadius: '999px'}} |
| Round the cards more | variables: {borderRadius: '16px'} |
| Change the font | variables: {fontFamily: 'Inter'} |
| Make the amount bigger and bolder | rules: {'.Amount': {fontSize: '36px', fontWeight: '700'}} |
| Tint the area behind the cards | variables: {colorPageBackground: '#F5F3FF'} |
| Hide "Powered by Advance" | showPoweredBy: false |
If you color the pay button or the selected tab without a text color, white or dark text is picked for you so it stays readable. Cards work differently: a card background that the default dark text can't be read on is ignored. For a dark or strongly colored card, also set colorText and colorTextSecondary to light colors, for example {colorBackground: '#111827', colorText: '#F9FAFB', colorTextSecondary: '#D1D5DB'}.
Font examples
Advance loads the font for you, so there's nothing to install. Pick one of these:
Inter · Roboto · Open Sans · Lato · Montserrat · Poppins · Nunito · Source Sans 3 · Work Sans · DM Sans
Inter: clean and neutral
Good when your site uses a modern system-style font.
const style = {
variables: {fontFamily: 'Inter'},
};Poppins: friendly and rounded
Good for consumer-facing brands. A semibold amount suits its geometric shapes.
const style = {
variables: {fontFamily: 'Poppins'},
rules: {'.Amount': {fontWeight: '600'}},
};Montserrat headings with Lato text: two fonts
Set the body font in variables, then use rules to give the heading and amount a display font.
const style = {
variables: {fontFamily: 'Lato'},
rules: {
'.Heading': {fontFamily: 'Montserrat', fontWeight: '700'},
'.Amount': {fontFamily: 'Montserrat', fontWeight: '700'},
},
};Font weights can be 400, 500, 600 or 700. Font sizes can be 10–48px, in px or rem.
Complete example
A full page you can copy. It gets a checkout URL from your backend, applies a brand style, and handles the result.
<!DOCTYPE html>
<html>
<head>
<title>Pay your invoice</title>
</head>
<body>
<iframe
id="advance-checkout"
style="width: 100%; max-width: 640px; height: 900px; border: none;"
allow="payment"
></iframe>
<script>
const ADVANCE_ORIGIN = 'https://app.advancehq.com';
// Your brand look
const style = {
variables: {
colorPrimary: '#5B2EFF', // pay button, selected tab, links
colorText: '#1F2937', // headings, values
colorTextSecondary: '#4B5563', // labels, subheading
colorPageBackground: '#F5F3FF', // area behind the cards
colorSurface: '#EEF2FF', // tab track
fontFamily: 'Poppins', // all text
borderRadius: '14px', // cards
},
rules: {
'.PayButton': {borderRadius: '999px', fontWeight: '600'},
'.PayButton:hover': {backgroundColor: '#4A22D9'},
'.Heading': {fontFamily: 'Montserrat', fontWeight: '700'},
'.Amount': {color: '#0F766E', fontWeight: '700'},
},
showPoweredBy: true,
};
async function startCheckout() {
// 1. Your backend creates the Plan or Payment Request and returns its URL
const res = await fetch('/api/create-checkout', {method: 'POST'});
const {checkout_url} = await res.json();
// 2. Load it in the iframe with embed mode and your style
document.getElementById('advance-checkout').src =
`${checkout_url}?embed=true&style=` +
encodeURIComponent(JSON.stringify(style));
}
// 3. React to what happens inside the checkout
window.addEventListener('message', event => {
if (event.origin !== ADVANCE_ORIGIN) return;
const {type, payload} = event.data;
if (type === 'advance.checkout.ready' && payload?.style_warnings) {
console.warn('Ignored style settings:', payload.style_warnings);
}
if (type === 'advance.checkout.success') {
// Confirm on your server before fulfilling (see Security)
window.location.href = `/confirmation?payment_id=${payload.payment_id}`;
}
if (type === 'advance.checkout.error') {
alert('Payment failed: ' + payload.error_message);
}
});
startCheckout();
</script>
</body>
</html>Good to know
- Each embed has its own style. Different pages or flows can look different.
- Styling only works inside an iframe with
embed=true. The hosted checkout, or the same link opened in its own tab, always shows the standard look. - Fixed parts. Padding, margins, borders, shadows and layout can't be changed. Nothing can be hidden, moved or resized, apart from font sizes.
- Readability is enforced. Amounts, the pay button text and legal text must keep at least 4.5:1 contrast against their background. A text color below that is ignored, and so is a card background that would make the default text hard to read. If
colorPrimaryis too light to read on your cards, it's still used for the button and tabs, but not for links and checkboxes. - Mistakes never break the checkout. Anything invalid is ignored, the rest of your style still applies, and the payer never sees an error. To see what was ignored, read
style_warningsin theadvance.checkout.readyevent (as in the complete example). A warning looks like["rules['.Amount'].padding: property not allowed"]. - Size limit. Keep the encoded
styleunder 8 KB; a typical one is about 1 KB. Astylelonger than 12,000 characters is ignored completely. - "Powered by Advance" link. "Advance" opens advancehq.com in a new tab. If your iframe has a
sandboxattribute, includeallow-popups allow-popups-to-escape-sandbox.
Reference
Variables
| Variable | Default | What it changes |
|---|---|---|
colorPrimary | #000000 | Pay button, selected payment-method tab, selected plan icon, checkbox, links |
colorPrimaryText | automatic | Text on primary-colored parts (white or dark is picked if you leave it out) |
colorText | #000000 | Headings, values, amounts, disclaimers |
colorTextSecondary | grey | Labels, subtitles, unselected tab, captions |
colorTextMuted | #A3A3A3 | "Secured with 256-bit encryption" line |
colorBackground | #FFFFFF | Cards |
colorPageBackground | transparent | Area behind the cards |
colorSurface | #F4F4F4 | Payment-method tab track, icon tiles |
colorSuccess | #54A96C | Success icon, highlighted monthly payment |
colorDanger | #FF4444 | Error messages |
fontFamily | Advance font | All text, from the font list above |
borderRadius | 8px | Cards and payment-plan cards |
Parts (for rules)
rules)| Part | What it is |
|---|---|
.Card, .SummaryCard, .PaymentCard | The invoice summary card and the payment card (background and corner radius only) |
.Heading, .Subheading | "Payment Details" / "Payment Plan" and the line under them |
.Label, .Value, .ValueSecondary | Invoice field labels, their values, and the billed-to email |
.Amount | The amount due |
.TabList, .Tab | The Credit Card / Bank Account toggle |
.PlanCard, .PlanTitle, .PlanAmount, .PlanAmountSuffix, .PlanCaption, .PlanRadio | Payment plan choices |
.Accordion, .AccordionTitle | Financing details |
.FeeRow, .FeeLabel, .FeeAmount, .TotalLabel, .TotalAmount | Fee breakdown above the button |
.PayButton | The pay button (and "Continue To Premium Financing") |
.Checkbox, .CheckboxLabel, .ErrorText | Premium financing terms checkbox |
.Link | Links (invoice download, policies, financing agreement) |
.Disclaimer | Policy sentence and ACH authorization text |
.SecureNote, .PoweredBy | The line under the pay button |
.Divider, .IconTile | Dividers and icon tiles |
.SuccessIcon, .SuccessTitle, .SuccessMessage | The success screen |
States
:hoverand:focus-visibleon any part, e.g..PayButton:hover.--selectedon.Tab,.PlanCard,.IconTileand.PlanRadio, e.g..Tab--selected.--disabledon.PayButton. While the button is disabled or processing, it stays the standard grey unless you style.PayButton--disabled.--highlighton.Value(the monthly payment in financing details).
Properties
| Property | Accepted values |
|---|---|
color | Text color: hex (#5B2EFF), rgb() or hsl(), fully opaque |
backgroundColor | Background color: hex, rgb() / rgba(), hsl() / hsla() or transparent |
fontFamily | One of the fonts listed above |
fontWeight | 400, 500, 600 or 700 |
fontSize | 10–48px, in px or rem (not on cards) |
borderRadius | px or rem, up to 999px. Only on .PayButton, .Card, .SummaryCard, .PaymentCard and .PlanCard |
Events Reference
The checkout sends these events to your page with window.postMessage, as { type, payload }, in both layouts. They always fire in this order:
ready ──▶ customer pays ──┬── success ──▶ settled
└── error ──▶ settled (customer stays on the form and can retry)
- Each payment attempt fires exactly one of
successorerror. A failed attempt can be followed by another attempt, so you may get severalerror+settledpairs before asuccess. settledalways fires right after, and carries no payload. Use it for cleanup that doesn't depend on the result: hiding spinners, logging.- After a successful payment the checkout shows its "Payment successful" screen inside the iframe.
| Event | When | Payload |
|---|---|---|
advance.checkout.ready | The checkout has loaded | None, or { style_warnings } if part of your style was ignored |
advance.checkout.success | The payment was accepted, or the invoice was already paid | { payment_id, amount, payment_method, status }, or { payment_id, already_paid: true } |
advance.checkout.error | The payment attempt failed | { error_code, error_message } |
advance.checkout.settled | After success or error | None |
success fields. A Plan checkout sends payment_id; a Payment Request sends payment_request_id instead.
| Field | Description |
|---|---|
payment_id / payment_request_id | The identifier from the checkout URL |
amount | The plan's invoiced amount or the payment request amount. It does not include processing fees. |
payment_method | credit_card or bank_account |
status | paid for card payments (funds received). processing for bank-account payments: the debit was authorized but hasn't settled. Settlement, or a later bounce, shows up in the payment's status in the Advance API. |
Financed plans: when the customer chooses monthly payments,
successfires after the down payment.payment_methodandstatusdescribe that down payment, not the whole invoice.
error fields. error_code and error_message are passed through from the payment processor when it returns them; their values depend on the processor. When it doesn't, which is common, they are generic fallbacks ("Unknown code" / "Unknown error" on a Plan checkout, "Unknown" / "Unknown error" on a Payment Request). Don't rely on specific codes; treat any error as "the attempt failed, the customer can retry".
When the link can't be paid, the checkout shows a status screen instead of the form:
| Situation | Screen | Event sent |
|---|---|---|
| Already paid | "Checkout Paid!" | success with already_paid: true (no ready) |
Expired (Payment Requests with a payment_expiration only) | "Payment Expired" | none |
| Cancelled | "Plan Cancelled" | none |
| Not found | "Payment Not Found" | none |
Example payloads
{"type": "advance.checkout.success",
"payload": {"payment_id": "pay_xyz789", "amount": 1250.00, "payment_method": "credit_card", "status": "paid"}}
{"type": "advance.checkout.success",
"payload": {"payment_id": "pay_xyz789", "amount": 1250.00, "payment_method": "bank_account", "status": "processing"}}
{"type": "advance.checkout.success",
"payload": {"payment_id": "pay_xyz789", "already_paid": true}}
{"type": "advance.checkout.error",
"payload": {"error_code": "Unknown code", "error_message": "Unknown error"}}
{"type": "advance.checkout.settled"}Configuration
URL parameters
| Parameter | Default | Description |
|---|---|---|
embed | false | true turns on embed mode: it hides tenant branding and sends payment events. |
fields_only | false | true shows only the Payment Details card. Only works in an iframe with embed=true. See Layouts. |
style | — | URL-encoded JSON that restyles the checkout. Only works in an iframe. See Styling Your Embed. |
Payment methods
Choose which payment methods appear by setting allowed_payment_methods when you create the Plan or Payment Request. If you leave it out, all available methods are shown.
| Method | Description |
|---|---|
checkout_credit_card | Credit or debit card |
checkout_ach | ACH bank transfer |
Security
Your API key stays on your server. The flow is:
- Your server creates the Plan or Payment Request with your API key.
- You get a checkout URL containing a unique, unguessable ID.
- Your page embeds that URL. The iframe needs no extra authentication.
Only someone with the URL can open the checkout, and only API key holders can create one. Each URL belongs to one payment and can't be reused after it's completed.
| Do this | Why |
|---|---|
| Create checkout URLs on your server | Never put your API key in browser code |
Check event.origin === 'https://app.advancehq.com' | Ignore messages from anyone else |
| Serve your site over HTTPS | Required to embed the checkout |
| Confirm the payment on your server | After a success event, call GET /v1/payments/{payment_id} (or GET /v1/payments/payment-request/{payment_request_id}) before fulfilling the order. For processing bank payments, wait for the payment to settle. |
Full Integration Example
The full flow with a plain iframe: your backend creates a Payment Request, and your frontend embeds it and handles the result. To add your own styling, add a style parameter as in the complete example.
Backend (Node.js)
// POST /api/create-checkout
app.post('/api/create-checkout', async (req, res) => {
const response = await fetch(
'https://api.advancehq.com/v1/payments/payment-request',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ADVANCE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: req.body.amount,
details: req.body.description,
recipient_email: req.body.email,
allowed_payment_methods: ['checkout_credit_card', 'checkout_ach'],
}),
},
);
const data = await response.json();
res.json({
checkout_url: data.payment_request_url,
payment_request_id: data.payment_request_id,
});
});Backend (Python)
import os
import httpx
from fastapi import APIRouter
router = APIRouter()
ADVANCE_API_KEY = os.environ["ADVANCE_API_KEY"]
@router.post("/api/create-checkout")
async def create_checkout(amount: float, description: str, email: str):
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.advancehq.com/v1/payments/payment-request",
headers={"Authorization": f"Bearer {ADVANCE_API_KEY}"},
json={
"amount": amount,
"details": description,
"recipient_email": email,
"allowed_payment_methods": ["checkout_credit_card", "checkout_ach"],
},
)
data = response.json()
return {
"checkout_url": data["payment_request_url"],
"payment_request_id": data["payment_request_id"],
}Frontend
<!DOCTYPE html>
<html>
<head>
<title>Checkout</title>
</head>
<body>
<h1>Complete Your Payment</h1>
<div id="checkout-container"></div>
<p id="status" hidden>Payment failed. Please try again.</p>
<script>
const ADVANCE_ORIGIN = 'https://app.advancehq.com';
async function startCheckout() {
// 1. Create the checkout through your backend
const res = await fetch('/api/create-checkout', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
amount: 1500.0,
description: 'Policy renewal',
email: '[email protected]',
}),
});
const {checkout_url, payment_request_id} = await res.json();
// 2. Mount the embedded checkout (payment fields only)
const iframe = document.createElement('iframe');
iframe.src = checkout_url + '?embed=true&fields_only=true';
iframe.style.cssText = 'width: 100%; height: 600px; border: none;';
iframe.allow = 'payment';
document.getElementById('checkout-container').appendChild(iframe);
// 3. Handle the result
window.addEventListener('message', async event => {
if (event.origin !== ADVANCE_ORIGIN) return;
const {type, payload} = event.data;
if (type === 'advance.checkout.success') {
// Confirm on your server before fulfilling
await fetch('/api/verify-payment', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
payment_request_id,
status: payload.status, // 'paid' or 'processing'
}),
});
window.location.href = '/confirmation';
}
if (type === 'advance.checkout.error') {
// The customer stays on the form and can try again
document.getElementById('status').hidden = false;
}
});
}
startCheckout();
</script>
</body>
</html>Environments
| Environment | Checkout Base URL | API Base URL |
|---|---|---|
| Sandbox | https://app.sandbox.advancehq.com | https://api.sandbox.advancehq.com |
| Production | https://app.advancehq.com | https://api.advancehq.com |
Build and test against Sandbox. It simulates payments without moving real money. Remember to use the matching origin in your event.origin check.
FAQ
Can I style the checkout to match my brand?
Yes. In embed mode the Advance tenant branding is hidden, and you can set your own colors, font and corner radius, either for the whole checkout or for single parts like the pay button. See Styling Your Embed.
Can I hide the invoice summary and show only the payment form?
Yes. Add fields_only=true together with embed=true. See Layouts.
How do I tell a finished card payment from a bank payment that is still clearing?
Read payment_method and status on the advance.checkout.success event: paid means the card payment was received; processing means the bank-account debit was authorized but hasn't settled. Track settlement through the payment's status in the Advance API.
Can I change the card number fields?
No. The payment processor draws those fields securely, and they always keep the standard look.
What if the customer closes the page mid-payment?
Nothing is charged until the customer submits the form. You can check the payment status through the API at any time.
Can I embed checkout in a mobile app?
The checkout posts its events to window.parent and only applies embed options inside an iframe, so it needs to run in one. In a WebView (Android) or WKWebView (iOS), load a small page you host that iframes the checkout with ?embed=true, listens for the message events, and forwards them to your app through your JavaScript bridge.
Is the embedded checkout PCI compliant?
Yes. Card and bank details go straight to our PCI-certified payment processor inside the iframe. They never touch your servers or ours.
Can I pre-fill customer information?
The billed-to name and email set when you create the Plan or Payment Request appear in the invoice summary card (full layout only). The payment fields themselves (card holder, card number, bank account) are not pre-filled.
What if a payment link expires?
Plan links never expire. A Payment Request link expires only if you set payment_expiration when you create the request. An expired link shows the customer a "Payment Expired" screen; no event is sent. Create a new Payment Request to get a fresh link.
Example Repository
For a complete working example, see the Advance Checkout Embed Example repository.
To try the embed without writing any code, open the Checkout Embed Playground, paste a checkout URL, and watch the events it sends.
Updated 9 days ago
