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

  1. Your server creates a Plan or a Payment Request with the Advance API and gets back a checkout URL.
  2. Your page shows that URL in an iframe, with ?embed=true added.
  3. Your customer pays inside the iframe (credit card or bank account).
  4. Your page receives a message (success, error, then settled) 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.

LayoutURLWhat the customer sees
Full (default)?embed=trueThe invoice summary card (invoice number, due date, billed to, amount due, invoice link) above the payment card.
Payment fields only?embed=true&fields_only=trueOnly 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_only only works inside an iframe with embed=true. The same link opened in its own tab (for example from the invoice email) shows the full checkout.
  • fields_only and style can be combined.

JavaScript SDK (Not available yet)

Not available yet. There is no JavaScript SDK for embedded checkout today: https://js.advancehq.com/checkout.js and AdvanceCheckout.create do not exist. Embed the iframe and listen for message events 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:

KeyWhat it does
variablesYour defaults for the whole checkout, e.g. "my brand color is purple"
rulesChanges for one specific part, e.g. "make only the pay button round". A rule wins over a variable.
showPoweredByfalse 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.

PartChange it everywhere (variables)Change only this part (rules)
CardscolorBackground, borderRadius.Card (both cards), .SummaryCard, .PaymentCard
Labels ("Billed To", "Due Date")colorTextSecondary.Label
Values (name, date; the email is .ValueSecondary)colorText.Value, .ValueSecondary
Amount duecolorText.Amount
Links (invoice download, policies)colorPrimary.Link
HeadingcolorText.Heading
SubheadingcolorTextSecondary.Subheading
Credit Card / Bank Account tabscolorPrimary (selected), colorSurface (track), colorTextSecondary (unselected).TabList, .Tab, .Tab--selected
Policy / ACH disclaimercolorText.Disclaimer
Pay buttoncolorPrimary, 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 cardscolorPageBackground(variable only)
Card and bank input fieldsCan'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 linksvariables: {colorPrimary: '#5B2EFF'}
Make only the pay button a different colorrules: {'.PayButton': {backgroundColor: '#0F766E'}}
Darken the button on hoverrules: {'.PayButton:hover': {backgroundColor: '#4A22D9'}}
Make the pay button pill-shapedrules: {'.PayButton': {borderRadius: '999px'}}
Round the cards morevariables: {borderRadius: '16px'}
Change the fontvariables: {fontFamily: 'Inter'}
Make the amount bigger and bolderrules: {'.Amount': {fontSize: '36px', fontWeight: '700'}}
Tint the area behind the cardsvariables: {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 colorPrimary is 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_warnings in the advance.checkout.ready event (as in the complete example). A warning looks like ["rules['.Amount'].padding: property not allowed"].
  • Size limit. Keep the encoded style under 8 KB; a typical one is about 1 KB. A style longer than 12,000 characters is ignored completely.
  • "Powered by Advance" link. "Advance" opens advancehq.com in a new tab. If your iframe has a sandbox attribute, include allow-popups allow-popups-to-escape-sandbox.

Reference

Variables

VariableDefaultWhat it changes
colorPrimary#000000Pay button, selected payment-method tab, selected plan icon, checkbox, links
colorPrimaryTextautomaticText on primary-colored parts (white or dark is picked if you leave it out)
colorText#000000Headings, values, amounts, disclaimers
colorTextSecondarygreyLabels, subtitles, unselected tab, captions
colorTextMuted#A3A3A3"Secured with 256-bit encryption" line
colorBackground#FFFFFFCards
colorPageBackgroundtransparentArea behind the cards
colorSurface#F4F4F4Payment-method tab track, icon tiles
colorSuccess#54A96CSuccess icon, highlighted monthly payment
colorDanger#FF4444Error messages
fontFamilyAdvance fontAll text, from the font list above
borderRadius8pxCards and payment-plan cards

Parts (for rules)

PartWhat it is
.Card, .SummaryCard, .PaymentCardThe 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, .ValueSecondaryInvoice field labels, their values, and the billed-to email
.AmountThe amount due
.TabList, .TabThe Credit Card / Bank Account toggle
.PlanCard, .PlanTitle, .PlanAmount, .PlanAmountSuffix, .PlanCaption, .PlanRadioPayment plan choices
.Accordion, .AccordionTitleFinancing details
.FeeRow, .FeeLabel, .FeeAmount, .TotalLabel, .TotalAmountFee breakdown above the button
.PayButtonThe pay button (and "Continue To Premium Financing")
.Checkbox, .CheckboxLabel, .ErrorTextPremium financing terms checkbox
.LinkLinks (invoice download, policies, financing agreement)
.DisclaimerPolicy sentence and ACH authorization text
.SecureNote, .PoweredByThe line under the pay button
.Divider, .IconTileDividers and icon tiles
.SuccessIcon, .SuccessTitle, .SuccessMessageThe success screen

States

  • :hover and :focus-visible on any part, e.g. .PayButton:hover.
  • --selected on .Tab, .PlanCard, .IconTile and .PlanRadio, e.g. .Tab--selected.
  • --disabled on .PayButton. While the button is disabled or processing, it stays the standard grey unless you style .PayButton--disabled.
  • --highlight on .Value (the monthly payment in financing details).

Properties

PropertyAccepted values
colorText color: hex (#5B2EFF), rgb() or hsl(), fully opaque
backgroundColorBackground color: hex, rgb() / rgba(), hsl() / hsla() or transparent
fontFamilyOne of the fonts listed above
fontWeight400, 500, 600 or 700
fontSize10–48px, in px or rem (not on cards)
borderRadiuspx 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 success or error. A failed attempt can be followed by another attempt, so you may get several error + settled pairs before a success.
  • settled always 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.
EventWhenPayload
advance.checkout.readyThe checkout has loadedNone, or { style_warnings } if part of your style was ignored
advance.checkout.successThe payment was accepted, or the invoice was already paid{ payment_id, amount, payment_method, status }, or { payment_id, already_paid: true }
advance.checkout.errorThe payment attempt failed{ error_code, error_message }
advance.checkout.settledAfter success or errorNone

success fields. A Plan checkout sends payment_id; a Payment Request sends payment_request_id instead.

FieldDescription
payment_id / payment_request_idThe identifier from the checkout URL
amountThe plan's invoiced amount or the payment request amount. It does not include processing fees.
payment_methodcredit_card or bank_account
statuspaid 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, success fires after the down payment. payment_method and status describe 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:

SituationScreenEvent 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

ParameterDefaultDescription
embedfalsetrue turns on embed mode: it hides tenant branding and sends payment events.
fields_onlyfalsetrue 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.

MethodDescription
checkout_credit_cardCredit or debit card
checkout_achACH bank transfer

Security

Your API key stays on your server. The flow is:

  1. Your server creates the Plan or Payment Request with your API key.
  2. You get a checkout URL containing a unique, unguessable ID.
  3. 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 thisWhy
Create checkout URLs on your serverNever put your API key in browser code
Check event.origin === 'https://app.advancehq.com'Ignore messages from anyone else
Serve your site over HTTPSRequired to embed the checkout
Confirm the payment on your serverAfter 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

EnvironmentCheckout Base URLAPI Base URL
Sandboxhttps://app.sandbox.advancehq.comhttps://api.sandbox.advancehq.com
Productionhttps://app.advancehq.comhttps://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.


Did this page help you?