Wallet Setup
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
[1] User lands on Wallet Page
This is the starting point. The user arrives at the page where the wallet payment process begins.
This is a basic example of a 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.
[2] Initiate the flow with beginFlow()
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.
[3] SDK determines the next required action
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 () => {
// [2] Call beginFlow to start the wallet transaction flow
const result = await lid.beginFlow({ checkoutId, txPayload });
// [3] 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
[4a] Confirming Transaction With Passkey
When an existing user has a registered passkey, beginFlow
will return nextAction: "passkey:tx"
. In that case,
you can display a page that allows the user to confirm the transaction using their passkey.
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 {
// [4a] 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.
[4b] Fallback Login And Passkey Autofill
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,
});
// [4b] 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) {
// [5a] 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
// [5a] 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.
[5b1] Fallback Login After Bank Redirect
This also applies to new users during onboarding.
After a user signs into their wallet via a fallback login (e.g., bank login), they are redirected back to the wallet app to complete the checkout flow. This section continues from the point where the bank login redirect completes.
Here’s an example of where the redirect might occur in the fallback component as it was shown previously:
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";
};
Before a passkey can be created, you'll need to perform an external authentication using a LoginID authorization token. This confirms the user's identity and resumes the checkout session.
After the user returns from the fallback login and lands on /post-login
, we validate the login and create an authorization
token. That token is then passed to the SDK to complete the external authentication step:
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 = ({ id }) => {
const navigate = useNavigate();
const [error, setError] = useState();
useEffect(() => {
const checkFlow = async () => {
try {
// Resume wallet session using session ID from bank redirect and fetch LoginID external auth token.
const walletResult = await continueSessionFromBankRedirect(id);
const { authorizationToken, callbackUrl } = await walletResult.json();
// [5b1] User signs in via fallback method
const result = await lid.performAction("external", { payload: authorizationToken });
if (result.nextAction === "passkey:reg") {
navigate.push("/add-passkey");
} else {
const errorPayload = ParseUtil.consertJSONToB64({ error: "Unconfirmed purchase" });
window.location.href = `${callbackUrl}?data=${errorPayload}`;
}
} catch (error) {
setError(error);
}
};
checkFlow();
}, []);
return error ? <p>{error}</p> : null;
};
The external
authentication step confirms the user's identity by exchanging an authorization token from your backend.
For details on requesting the LoginID token, see the MFA fallback section.
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.
[5b2] 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" });
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 {
// [5b2] Create a new passkey after fallback login
const result = await lid.performAction("passkey:reg");
if (result.accessToken) {
// [5c] 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.
Discovery Page
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
discover
method. - Use
postMessage
to 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;
};
Having issues? Visit our troubleshooting section for common questions and solutions.