Wallet Setup (Web JavaScript)
A wallet is the interface your users interact with to authenticate and complete transactions - typically hosted on a dedicated domain like wallet.example.com. It handles passkey authentication and transaction approval.
The Wallet SDK supports two key user scenarios involving MFA and passkeys:
- Creating a passkey as part of your existing banking sign-up/sign-in flow
- Using that passkey to authenticate payment actions
Prerequisites
- Create an application to obtain a base URL. The SDK uses this base URL to interact with the LoginID authentication service.
- Create an API key with at least the external:verify scope. You’ll need this to request authorization tokens from your backend.
Setup SDK
- Javascript
npm i @loginid/checkout-wallet
Import the class:
import { LoginIDWalletAuth } from "@loginid/checkout-wallet";
const lid = new LoginIDWalletAuth({
baseUrl: process.env.REACT_APP_LOGINID_BASE_URL,
});
You can view the source code here.
Wallet Payment Page Overview
This overview outlines how the Wallet SDK determines the next user action (e.g., transaction confirmation with passkey or fallback login)
once the checkout flow begins. Using checkoutId and txPayload, the wallet identifies the correct path and guides
the user accordingly — either confirming the payment with a passkey, or presenting a sign-in step (via passkey or a
fallback method such as bank login).
The overview diagram is numbered to match key steps throughout the rest of this section. You can follow along each action (e.g., [2], [3], [4a], etc.) in the flowchart as you read through the related code and explanations that follow.
[0] Discovery Page - Determine Flow
There are two ways to integrate depending on your merchant setup:
- 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 merchant library.
Select the tab below based on your preferred approach.
- With LoginID Merchant Library
- Without LoginID Merchant Library
The discovery page must be hosted on the wallet domain. Its purpose is to determine whether an embedded checkout flow (via iframe) is supported, or if the experience should fall back to your preferred checkout fallback method (e.g., redirect).
It's important that the discovery method is invoked immediately on page load, to ensure the fastest possible decision between embedded and fallback flows.
Here’s an example using React:
import { useEffect } from "react";
import { LoginIDWalletAuth } from "@loginid/checkout-wallet";
const config = {
baseUrl: process.env.REACT_APP_LOGINID_BASE_URL
};
const lid = new LoginIDWalletAuth(config);
const WalletDiscoveryPage = () => {
useEffect(() => {
const runDiscovery = async () => {
await lid.discover();
};
runDiscovery();
}, []);
};
The discovery page must be hosted on the wallet domain. Its purpose is to determine whether an embedded checkout flow (via iframe) is supported, or if the experience should fall back to your preferred checkout fallback method (e.g., redirect).
To implement the discovery flow:
- Create a hidden iframe on the merchant domain that loads the discovery page from the wallet domain.
- On iframe load, call the Wallet SDK’s
discovermethod. - Use
postMessageto communicate the result back to the merchant page, where it can decide how to proceed.
It's important that the discovery method is invoked immediately on page load, to ensure the fastest possible decision between embedded and fallback flows.
Here’s an example using React:
import { useEffect } from "react";
import { LoginIDWalletAuth } from "@loginid/checkout-wallet";
const config = {
baseUrl: process.env.REACT_APP_LOGINID_BASE_URL
};
const lid = new LoginIDWalletAuth(config);
const WalletDiscoveryPage = () => {
useEffect(() => {
const runDiscovery = async () => {
try {
const result = await lid.discover();
// Send result to parent window (merchant domain)
window.parent.postMessage(
{ type: "discover_result", payload: result },
merhcantUrl
);
} catch (error) {
console.error("Discovery failed", error);
}
};
runDiscovery();
}, []);
return null;
};
[1] Initiate the Flow With beginFlow - SDK Determines the Next Required Action
This is the starting point. The user arrives at the page where the wallet payment process begins.
The code below demonstrates a basic wallet payment page. Once the checkoutId is passed to beginFlow, the Wallet
SDK determines the next required action. Based on the response, you decide what to display to the user.
The SDK determines the next step by calling beginFlow with checkoutId and txPayload.
This should be called initiated as soon as the page loads, such as within a useEffect or a similar lifecycle method.
txPayload and checkoutId?txPayload is a string-based representation of the transaction being authorized. It can be a plain JSON string or
an encoded/encrypted format that you define. This payload is included within a challenge and signed by the user's passkey,
acting as proof that the user explicitly approved the transaction.
Depending on your implementation, txPayload can be generated on the wallet domain or passed in from the merchant
domain (e.g., via iframe postMessage or a redirect method).
checkoutId is explained in more detail here.
The result includes nextAction, which guides the UI toward either a passkey transaction or a fallback login.
import React, { useEffect, useState } from "react";
import { LoginIDWalletAuth } from "@loginid/checkout-wallet";
const lid = new LoginIDWalletAuth({
baseUrl: process.env.REACT_APP_LOGINID_BASE_URL,
});
const WalletPage = ({ checkoutId, txPayload }) => {
const [action, setAction] = useState<string | null>(null);
const checkFlowHandler = async () => {
// [1] Call beginFlow to start the wallet transaction flow
const result = await lid.beginFlow({ checkoutId, txPayload });
// [1] The SDK decides what action to take next
switch (result.nextAction) {
case "passkey:tx":
// Show transaction confirmation UI
setAction("transaction");
break;
default:
// Show a fallback UI
setAction("fallback");
break;
}
}
useEffect(() => {
checkFlowHandler();
}, [checkFlowHandler, checkoutId, txPayload]);
return (
<div>
{action === "fallback" && <Fallback />}
{action === "transaction" && <TxConf />}
</div>
);
};
The SDK determines the appropriate action using checkoutId and other context from the wallet.
The following sections provide example implementations for each possible next step and how they can integrate with your existing setup.


Checkout Flows
[2a] Confirming Transaction With Passkey
When an existing user has a registered passkey, beginFlow will return nextAction: "passkey:tx". In this step, the user
is not only signing in with their passkey, but also explicitly approving the payment for the selected items.
You can override the txPayload during confirmation by passing a new payload to performAction:
await lid.performAction("passkey:tx", { txPayload: "<TX_PAYLOAD>" });
This lets you update transaction details right before user approval.
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { finalizeTransaction } from "./services/wallet";
import { LoginIDWalletAuth } from "@loginid/checkout-wallet";
const lid = new LoginIDWalletAuth({
baseUrl: process.env.REACT_APP_LOGINID_BASE_URL,
});
const TxAuth = () => {
const navigate = useNavigate();
const [error, setError] = useState("");
const txConfHandler = async () => {
try {
// [2a] Confirm transaction using passkey
const result = await lid.performAction("passkey:tx");
if (result.payloadSignature) {
// Finalize transaction by verifying payloadSignature and returning to merchant
await finalizeTransaction(result.payloadSignature, true);
// Transaction process is complete return back to merchant now.
} else {
navigate("/error");
}
} catch (e: any) {
setError(e.message || "Passkey registration failed.");
}
};
return (
<div>
<button onClick={txConfHandler}>Pay Your Order</button>
{error && <p>{error}</p>}
</div>
);
};


Once confirmed, the user has successfully approved their payment using a passkey. On future checkouts from this device, LoginID will remember this approval and allow the flow to go directly to transaction confirmation.
After confirming the transaction, finalize it using the Finalizing Result to Merchant step.
For details on validating LoginID's payloadSignature, see the
transaction confirmation backend section.
[2b] Wallet Passkey Signin
If LoginID can't detect that a user has a passkey, show a fallback login. You can also optionally trigger passkey autofill—useful when the user has a registered a passkey but is signing in from a different device or browser synced through a password manager.
This is a common first-time scenario on a new device.
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { finalizeTransaction } from "./services/wallet";
import { LoginIDWalletAuth } from "@loginid/checkout-wallet";
const lid = new LoginIDWalletAuth({
baseUrl: process.env.REACT_APP_LOGINID_BASE_URL,
});
// [2b] Fallback flow component for login and passkey autofill
const Fallback = () => {
const router = useRouter();
const [error, setError] = setState("");
const [username, setUsername] = useState("");
useEffect(() => {
const autofill = async () => {
try {
// Passkey autofill authentcation
const result = await lid.performAction("passkey:auth", { autoFill: true });
if (result.accessToken) {
// [3a] Autofill was successful, continue to finalize payment
await finalizeTransaction(result.accessToken, true);
}
} catch (e) {
setError(e.message);
}
};
autofill();
}, []);
const bankLoginHandler = (e) => {
e.preventDefault();
// Redirect to external bank login or some other method for identity
};
const passkeyUsernamelessLoginHandler = () => {
e.preventDefault();
try {
// This initiates passkey usernameless authentication.
// After the button is clicked, the user will be prompted to select a passkey profile.
const result = await lid.performAction("passkey:auth");
if (result.accessToken) {
// Optional: verify result.accessToken with your backend here
// [3a] Autofill was successful, continue to finalize payment
await finalizeTransaction(result.accessToken, true);
}
} catch (e) {
setError(e.message);
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
id="username"
name="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
autoComplete="username webauthn"
/>
<button onClick={bankLoginHandler}>Sign In With Your Bank</button>
<button onClick={passkeyUsernamelessLoginHandler}>Sign In With Your Passkey</button>
</form>
{error && <p>{error}</p>}
</div>
);
};


If the user completes autofill successfully, they're signed into their wallet and can proceed to payment confirmation. Since a passkey was used, the next session will skip fallback login and go straight to confirmation.
If the user signs in via a fallback login (like bank login), you can guide them to create a passkey for future frictionless access.
After passkey login, complete the flow by following the Finalizing Result to Merchant step.
[3b-1] Fallback Login or New User Onboarding After Bank Redirect
If the user doesn't have a passkey or is on a new device, the wallet may fall back to a traditional login method — such as bank login — as shown in [2b].
After completing the fallback login, the user is redirected back to the wallet app to continue the checkout flow. This section picks up from that point — when the user returns from an external identity provider (e.g., a bank login page).
To keep things simple, the following example just builds on the previous section of
bankLoginHandler. In practice, you’d want to use a more secure and production-ready approach.
const bankLoginHandler = (e) => {
e.preventDefault();
// Redirect to external bank login or some other method for identity
window.location.href = "https://bank.example.com/login?session_id=abc123&redirect_uri=https://wallet.example.com/post-login";
};
The remaining section covers what happens after a successful bank login — including how to resume the session.
Now that the user has returned to the wallet domain (e.g., /post-login), the next step is to verify their login,
resume the session, and optionally offer to register a passkey.
But before a passkey can be created, you must perform an external authentication using a LoginID authorization token. This confirms the user's identity and resumes the checkout session.
After the user successfully authenticates with their bank, your backend should validate the login and request an authorization token from LoginID. This token is used to resume the wallet session and optionally prompt the user to create a passkey.
The flow typically looks like this:
- User is redirected to your bank's login page.
- After login, the user is redirected back to the wallet (e.g.,
/post-login) with a session identifier. - Your backend verifies the session and makes a POST request to
/fido2/v2/mgmt/grant/external-authto get an authorization token from LoginID. This is where your API key listed on the prerequisites will be needed. See example implementation in code. - The frontend passes this token to the Wallet SDK to complete the authentication process via
performAction("external", { payload: token }).
This allows the session to resume and enables passkey registration for future passwordless access.
Once the authorization token has been obtained from your backend, pass it to the Wallet SDK to complete the external authentication process:
import { useEffect, useState } from "react";
import { LoginIDWalletAuth } from "@loginid/checkout-wallet";
import { continueSessionFromBankRedirect } from "./services/wallet";
import ParseUtil from "./utils/";
const lid = new LoginIDWalletAuth({
baseUrl: process.env.REACT_APP_LOGINID_BASE_URL,
});
const PostBankLogin = () => {
const navigate = useNavigate();
const [error, setError] = useState();
useEffect(() => {
const checkFlow = async () => {
try {
// Example only — this function is not part of the SDK.
// It represents your own logic to call your backend after a successful bank login.
// Your backend should validate the login and return a LoginID authorization token.
const walletResult = await continueSessionFromBankRedirect();
const { authorizationToken, callbackUrl } = await walletResult.json();
// [3b-1] User signs in via fallback method
const result = await lid.performAction("external", { payload: authorizationToken });
if (result.nextAction === "passkey:reg") {
navigate.push("/add-passkey");
} else {
// Continue without adding passkey to user
}
} catch (error) {
setError(error);
}
};
checkFlow();
}, []);
return error ? <p>{error}</p> : null;
};
Once authenticated, this example redirects the user to the main PasskeyCreate component. Your implementation may differ depending on how you handle routing and component structure.
[3b-2] Optionally Create a Passkey
You can pass in a display name during passkey creation.
The display name is a user-friendly label that helps users recognize the correct passkey from a list, like a familiar nickname.
await lid.performAction("passkey:reg", { displayName: "Wallet User" });
This is a follow-up from the previous section. We mavigate to the PasskeyCreate view after verifying bank login and finishing external authentication.
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { LoginIDWalletAuth } from "@loginid/checkout-wallet";
const lid = new LoginIDWalletAuth({
baseUrl: process.env.REACT_APP_LOGINID_BASE_URL,
});
const PasskeyCreate = () => {
const navigate = useNavigate();
const [error, setError] = useState("");
const addPasskeyHandler = async () => {
try {
// [3b-2] Create a new passkey after fallback login
const result = await lid.performAction("passkey:reg");
if (result.accessToken) {
// [3b-3] Continue to finalize payment
await finalizeTransaction(result.accessToken, true);
}
} catch (e: any) {
setError(e.message || "Passkey registration failed.");
}
};
const skipHandler = () => {
// default if user does not want to add a passkey
};
return (
<div>
<button onClick={addPasskeyHandler}>Create Passkey</button>
<button onClick={skipHandler}>Skip for now</button>
{error && <p>{error}</p>}
</div>
);
};


After the passkey is optionally created, complete the flow by following the Finalizing Result to Merchant step.
Finalize and Return Result to Merchant
Once the transaction has been confirmed (via passkey or fallback flow), you should finalize the payment on your wallet backend and return the result to the merchant application.
Here’s an example of how you might handle this:
// Example: Finalizing transaction result and returning to merchant
export const finalizeTransaction = async (payloadToken: string, embed: boolean) => {
// Confirm the payment on your wallet backend
const result = await walletBackend.confirmPayment(payloadToken);
if (embed) {
// Post message to parent merchant window (iframe scenario)
// `result.passkey` may indicate that user has used a passkey
window.parent.postMessage(
{
type: "payment_result",
token: result.token,
passkey: result.passkey
},
"*"
);
} else {
// Redirect user back to merchant with encoded result
const callbackUrl = `${result.callback}?${new URLSearchParams({
token: result.token,
passkey: result.passkey
}).toString()}`;
window.location.href = callbackUrl;
}
};
You can call this after authentication or payment confirmation.
Conclusion
The Wallet SDK streamlines checkout by combining authentication and payment approval into a single flow.
With beginFlow, the SDK determines the right next action — either confirming the transaction with a passkey or
guiding the user through fallback login.
Passkey-based transaction confirmation serves as both signin and checkout approval in one step, delivering a fast and secure experience for returning users. Fallback methods and passkey creation ensure new or recovering users can still complete their transactions.
This provides a smooth, secure checkout that reduces friction while strengthening trust.
For additional support or clarification, you can reach us anytime at support@loginid.io.
Having issues? Visit our troubleshooting section for common questions and solutions.