Merchant Setup
A merchant is any business or platform integrating checkout into their website — for example, an online store selling clothing or a booking site for events.
The Merchant SDK provides the tools that determine the checkout flow. If conditions are met, an embedded iframe delivers a seamless, on-site experience with cross-domain passkey authentication and integrated UI. If not, it will gracefully falls back to a custom login, such as a full-page redirect to a bank login.


Integration
You’ll need to implement:
- A custom
<iframe>setup postMessagecommunication between merchant and wallet domains- A fallback for cases where embedding isn’t recommended
Basic Flow
-
Start with a discovery page
Use the Wallet SDK's
discovermethod to determine if passkey login is available. -
Generate a
checkoutIdCreate or retrieve a
checkoutId, and pass it to the Wallet in the iframe URL or viapostMessage. -
Track embedded completion
After a successful embedded checkout, set a persistent client-side flag (e.g., in localStorage) on the merchant domain.
What is checkoutId?
A checkoutId is a cryptographic identifier that links a wallet account to a browser that has successfully completed passkey registration.
It serves two purposes:
- Account Association — it allows the wallet to determine which wallet account the checkout belongs to.
- Possession — it proves that the browser participating in the checkout is the same browser that previously registered the passkey.
The checkoutId is passed from the merchant domain to the wallet domain during checkout. It is cryptographically signed and bound to the browser where passkey registration occurred, allowing the wallet to verify both account association and possession of the browser that originally registered the passkey.
How is a checkoutId Generated?
- The merchant generates a browser-bound key pair using the Web Crypto API.
- The private key is stored as a non-exportable key in IndexedDB.
- The browser signs a checkout payload using the stored private key.
- The JWS header contains a fingerprint or identifier for the associated public key.
- The resulting JWS becomes the
checkoutId. - The wallet validates the signature and uses the public key fingerprint to resolve the associated account.
This approach ensures that only the browser that originally completed passkey registration can generate valid checkoutId signatures.
and signing process.
Discovery Page
The discovery page must be hosted on the wallet domain. It’s responsible for determining whether an embedded checkout is possible.
To implement it:
- Create a hidden iframe that points to the discovery page.
- On load, call the Wallet SDK’s discover method.
- Use
postMessageto return the result to the merchant domain.
Based on the result, decide how to proceed:
- If
DiscoveryResult.flowisEMBED, continue with the embedded checkout. - Otherwise, trigger your fallback flow (e.g. redirect to wallet).
Pass CheckoutId to Wallet
Before starting the embedded or fallback checkout flow, you must pass a checkoutId to the wallet domain.
- If a
checkoutIdalready exists locally, reuse it. - If not, generate a new one and send it to the wallet.
This identifier is a key part of the checkout session and helps associate the user with the transaction.
Flagging Completed Embedded Flow
Once a user completes a checkout session via the embedded flow, you should persist a flag on the merchant
domain (e.g., using localStorage).
This flag signals that the user has previously completed an embedded checkout on this browser and likely has a passkey available.
Use this flag to optimize future checkouts:
-
If the flag exists:
Skip discovery and proceed directly with the embedded checkout flow.
-
If the flag is missing:
- Run the Wallet SDK’s discovery flow via the discovery page.
- If the result is
EMBEDDED_CONTEXT, proceed with embedded checkout. - Otherwise, fall back to your default checkout method.
Full Example
Here are somes examples of how your setup might look on the merchant side:
There are two ways to integrate:
- With LoginID Merchant Library – Recommended for most merchants. Offers built-in utilities for managing
checkoutId, iframe messaging, and discovery. - Without LoginID Merchant Library – Use this if you need full control or want to integrate directly without relying on the library.
Select the tab below based on your preferred approach.
- With LoginID Merchant Library
- Without LoginID Merchant Library
Setup:
- Javascript
Install with command:
npm i @loginid/checkout-merchant
Example:
import { CheckoutIdStore, ParentMessages } from "@loginid/checkout-merchant";
const walletDiscoveryUrl = "https://wallet/discover";
// Step 1: Create a hidden iframe pointing to the discovery page hosted on the wallet domain.
// This allows the merchant to determine if embedded checkout is supported.
const hiddenIframe = document.createElement("iframe");
hiddenIframe.width = "0";
hiddenIframe.height = "0";
hiddenIframe.style.display = "none";
hiddenIframe.src = walletDiscoveryUrl;
// Step 2: Check if a user has already completed an embedded checkout.
// If true, we can skip discovery and go straight to embedding.
const hasBeenEmbedded = localStorage.getItem("lid-checkout");
const body = document.querySelector("body")
// Step 3: Inject hidden discovery iframe into the page.
body?.appendChild(hiddenIframe);
const store = new CheckoutIdStore();
// Step 4: Generate or retrieve a persistent checkoutId.
const checkoutId = await store.setOrSignWithCheckoutId();
const messages = new ParentMessages(hiddenIframe);
// Step 5: If the user has previously completed embedded checkout, skip discovery.
// Otherwise, run discovery via postMessage to determine if embedded flow is possible.
const result = hasBeenEmbedded
? { flow: "EMBED" }
: await messages.sendMessage("discover") as any;
if (result.flow === "EMBED") {
const walletUrl = "https://wallet";
// Step 6: Proceed with embedded checkout by injecting a new iframe to load the wallet.
const iframe = document.createElement("iframe");
iframe.width = "90%";
iframe.height = "90%";
iframe.style.display = "block";
iframe.allow =
"publickey-credentials-get *; publickey-credentials-create *; bluetooth";
iframe.src = walletUrl;
// Remove the discovery iframe since it's no longer needed.
hiddenIframe.remove();
body?.appendChild(iframe);
const messages = new ParentMessages(iframe);
// Step 7: Pass the checkoutId to the wallet to initiate the signing process.
const result = await messages.sendMessage("sign_transaction", { checkoutId }) as any;
if (result.error) {
// Step 8: Handle signing failure by removing iframe and throwing an error.
iframe.remove();
throw new Error(result.error);
}
// Step 9: On success, set a flag in localStorage to remember that the user has used embedded checkout.
localStorage.setItem("lid-checkout", "true");
// Optional: Remove iframe after successful checkout flow.
iframe.remove();
return
} else {
// Step 10: Handle fallback scenario if embedded checkout is not available.
// This might include redirecting the user to a full-page wallet login.
}
Here is a sample from our checkout demo that uses a custom approach for communicating with the iframe and
handling the transfer of the checkoutId.
This approach doesn't rely on the Merchant library directly.
See the implementation details here.
Need help integrating secure embedded checkout on your merchant domain? Reach out to our support team at support@loginid.io for personalized assistance.
Having issues? Visit our troubleshooting section for common questions and solutions.