Skip to main content
New. Embedded checkout runs the same Laces checkout as hosted checkout, in a modal instead of a redirect. Changes to laces.js are listed in the changelog.
You add one script, call LacesCheckout.open() from a button, and Laces checkout opens in an overlay on top of your page. When the buyer finishes, the modal tells your page, and your server verifies the result. You need: an API key with Start hosted checkout (checkout_sessions:write), and your page’s exact origin in Allowed domains. Return URL domains are not needed for embedded checkout.
Every key, token and id on this site is a made-up example value. Your real keys are issued to you in the Laces app, under Settings → Developers.

The flow

1. Add the script

Load it from https://laces.run/v1/laces.js. Don’t self-host it or copy it into your bundle: the script works out which Laces site to talk to from its own src URL, and only accepts messages from that origin. A self-hosted copy reports misconfigured or opens a frame that never loads. The script has no dependencies, is under 6 KB, and defines one global, LacesCheckout.

2. Open the checkout

With a publishable key, laces.js creates the session for you: a POST https://connect.laces.run/v1/checkout-sessions with ui_mode: "embedded". The API takes your page’s Origin as the session’s embed origin, so that origin must be in Allowed domains, or the call fails with 403 origin_not_allowed. The overlay appears immediately with a “Loading secure checkout…” spinner while the session is created and the checkout loads. Only one checkout can be open at a time; calling open() while one is open does nothing.
A 'Secure checkout' modal showing the Laces Register and Pay form for Kampala City 10K, over a dimmed event page

The checkout open in its modal over the partner's page (desktop). The example site and race are made up.

On phones the modal fills the screen:
The Laces checkout form filling a narrow phone screen, with a close button at the top

The same checkout on a 390 px wide phone screen.

Options

LacesCheckout.close() closes the open checkout from your own code (it fires onClose). It does nothing if no checkout is open.

Callbacks

A callback that throws doesn’t break the modal; the error is re-thrown asynchronously so it still shows in your console.

Creating the session on your server

To keep the publishable key off the page, or to set client_reference_id and prefill on the server, create an embedded session with a bearer token and pass only its id to the page:
Rules for embedded sessions:
  • ui_mode: "embedded".
  • embed_origin is required with a bearer token: the exact origin of the page that will show the modal. It must be in Allowed domains, or you get 400 invalid_embed_origin. With a publishable key it is ignored, and the request’s own Origin is used.
  • on_success_url and on_cancel_url are optional. If you send them, the return URL rules apply. (An empty string is not “omitted”: leave the field out instead.)
  • The session can be shown only inside a page on that one origin. Laces pins the checkout’s frame-ancestors to it, so the same session in a frame on any other site is blocked by the browser.
  • Sessions expire after 24 hours. Create one per click.

3. Messages from the modal

The checkout talks to your page with postMessage. laces.js turns these into your callbacks, so most integrations never handle them directly.
  • Laces always posts to your page’s exact origin (the session’s embed origin), never *.
  • laces.js only accepts messages whose event.origin is the Laces site it was loaded from and whose source is its own iframe.
  • An embedded success never redirects your page.

4. Verify on your server

Never fulfil an order on the browser’s word alone. A redirect to your success page, or an onSuccess callback in the page, is user experience, not proof of payment. Anyone can type your success URL. Before you mark anything paid, confirm it on your server: read the session with GET /checkout-sessions/{id} and require status: "complete", or act on a signed order.paid webhook.
onSuccess runs in the browser, which the buyer controls. Treat it as a cue to ask your server, exactly like the hosted checkout return:

What the buyer sees

The buyer pays with mobile money (MTN or Airtel) today; card is coming soon. Inside the modal they see the total with every fee, enter their number, and approve the prompt on their phone.
The modal showing subtotal UGX 50,000, Laces Service Fee UGX 1,950 and total UGX 51,950, above a Pay with mobile money section

The payment step inside the modal. This screenshot was taken in test mode, which is why it shows a sandbox banner; live checkouts don't.

When the payment is confirmed, the modal shows the order code and sends laces:success, so your onSuccess fires. The modal stays open until the buyer taps Done or closes it.
A green 'Payment received' panel reading 'You've successfully paid for Kampala City 10K' with an order code and QR code

Payment received, inside the modal (test mode, hence the banner).

The confirmation's 'What happens next' note, a Done button, a View your ticket and QR button, and Save PDF, Share, Receipt and Save image buttons

Further down the confirmation: the ticket links and the Done button, which closes the modal and fires onClose.

A free ticket skips payment: the buyer confirms their entry and gets the same confirmation.
The modal headed 'Confirm your entry for Kampala City 10K' with a note that the entry is free and the total shown as Free

The confirm step for a free entry: no payment and no fees.

Your order.paid webhook fires either way, so fulfil from the webhook or a server-side session read, never only from onSuccess.
When card is enabled. Card will open the card provider’s own secure window inside the modal. If a browser won’t show that window inside a frame, the buyer gets a “Continue in a new tab” link to the same order on laces.run; the modal keeps watching the order, so onSuccess still fires when they pay in the tab.

Content Security Policy

If your site sends a Content-Security-Policy, allow:
  • script-src https://laces.run loads laces.js.
  • frame-src https://laces.run lets the checkout iframe load (on older browsers, child-src).
  • connect-src https://connect.laces.run lets laces.js create the session with your publishable key. Not needed if you only open server-created sessions with sessionId.
laces.js sets its styles through the DOM (style.cssText), so a strict style-src without 'unsafe-inline' doesn’t break the overlay.

Accessibility and behaviour

  • The overlay is a role="dialog" with aria-modal, keeps keyboard focus inside it, and stops the page behind it from scrolling. When it closes, focus returns to the element that opened it.
  • On small screens it fills the viewport; on larger ones it is a centred panel.
  • The iframe is not sandboxed and carries allow="payment", which card payments need.
  • Embedded checkout doesn’t rely on third-party cookies, so it works in browsers that block them.

Local development

Your page must be on an origin in Allowed domains, and that includes your dev server. Add its exact origin, for example http://localhost:3000 (or http://127.0.0.1:3000), and the embedded checkout opens and completes there with your live keys, end to end. The port is part of the origin, so http://localhost:5173 needs its own entry. A domain you have just added can take up to 5 minutes to pass the browser’s CORS check. See Local development domains and Testing your integration.

Errors

onError receives { error_code, message, status }. status is the HTTP status, or 0 when there was no response. For problems found inside the overlay, the modal shows message to the buyer and stays open until they close it. The API error codes are the same as for creating a checkout session; see also Errors.

Troubleshooting

When the key or the page origin is wrong, the modal opens and shows the problem in place of the form, and onError fires with the matching code.
An empty Secure checkout modal with the message 'Invalid publishable key'

invalid_key: the publishable key is wrong, was replaced by Regenerate keys, or the key was revoked.

An empty Secure checkout modal with the message 'This origin is not allowed for this key'

origin_not_allowed: this page's exact origin isn't in the key's Allowed domains.