Skip to Content
We're improving our docs. Share your experience and help shape what comes next.
DocumentationLedger Wallet ProviderGet Started

Get started with Ledger Wallet Provider

This guide shows you how to add Ledger Wallet Provider to a web application and request accounts through the EIP-6963 provider discovery protocol.

Partner program: Ledger Wallet Provider requires enrollment in Ledger’s partner program to obtain API keys. Contact our team to discuss integration.

Before you start, check the Requirements to confirm your target browsers support Web HID and Web Bluetooth.

Install the SDK

npm install @ledgerhq/ledger-wallet-provider

You can also install with yarn or pnpm.

Initialize the provider

import { initializeLedgerProvider } from '@ledgerhq/ledger-wallet-provider' import '@ledgerhq/ledger-wallet-provider/styles.css' const cleanup = initializeLedgerProvider({ dAppIdentifier: 'my-dapp', // Your dApp identifier apiKey: 'your-api-key', // Your Ledger API key loggerLevel: 'info', // Log level: 'fatal', 'error', 'warn', 'info', 'debug' target: document.body, // Where to mount the UI (default: document.body) dmkConfig: undefined, // Device Management Kit configuration (optional) devConfig: { stub: { base: false, // Set to true to enable base stub mode account: false, // Set to true to stub account operations device: false, // Set to true to stub signer interactions web3Provider: false, // Set to true to stub Web3 provider responses dAppConfig: false, // Set to true to stub dApp configuration }, }, })

initializeLedgerProvider mounts the Ledger UI and announces the Ledger wallet to every supported chain: EVM dApps via the EIP-6963  discovery protocol, and Solana dApps via the Wallet Standard . It returns a cleanup function — call it when your application unmounts to remove the UI and fully unregister every chain provider.

The example below discovers the provider over EIP-6963 for an EVM dApp. Solana dApps discover the same wallet automatically through Wallet Standard libraries such as @solana/wallet-adapter — no extra wiring required.

See Configuration for the full list of available options.

Discover the provider (EIP-6963)

Listen for provider announcements before dispatching the request event:

window.addEventListener('eip6963:announceProvider', (event) => { const { provider, info } = (event as CustomEvent).detail console.log('Ledger provider discovered', info.name) // Store provider for later use }) window.dispatchEvent(new Event('eip6963:requestProvider'))

Request accounts

const accounts = await provider.request({ method: 'eth_requestAccounts', params: [], })

Once users connect their signer through the Ledger Wallet Provider flow, you receive accounts and can start signing transactions or messages.

Complete React example

Here’s a full working example that demonstrates provider discovery, account connection, and transaction signing:

import { useEffect, useState, useCallback } from 'react' import type { EIP6963ProviderDetail } from '@ledgerhq/ledger-wallet-provider' function useProviders() { const [providers, setProviders] = useState<EIP6963ProviderDetail[]>([]) const [selectedProvider, setSelectedProvider] = useState<EIP6963ProviderDetail | null>(null) const handleAnnounceProvider = useCallback((e: CustomEvent<EIP6963ProviderDetail>) => { setProviders(prev => { const found = prev.find(p => p.info.uuid === e.detail.info.uuid) if (found) return prev return [...prev, e.detail] }) }, []) useEffect(() => { // Dynamic import is required because the library uses browser APIs // and won't work with Server-Side Rendering (SSR) const initializeProvider = async () => { await import('@ledgerhq/ledger-wallet-provider/styles.css') const { initializeLedgerProvider } = await import('@ledgerhq/ledger-wallet-provider') const cleanup = initializeLedgerProvider({ dAppIdentifier: 'my-dapp', apiKey: 'your-api-key', loggerLevel: 'info', }) window.addEventListener('eip6963:announceProvider', handleAnnounceProvider) return cleanup } let cleanup: (() => void) | undefined initializeProvider().then(cleanupFn => { cleanup = cleanupFn }) return () => { cleanup?.() window.removeEventListener('eip6963:announceProvider', handleAnnounceProvider) } }, [handleAnnounceProvider]) return { providers, selectedProvider, setSelectedProvider } } function App() { const { providers, selectedProvider, setSelectedProvider } = useProviders() const [account, setAccount] = useState<string | null>(null) const connectWallet = async () => { if (!selectedProvider) return try { const accounts = await selectedProvider.provider.request({ method: 'eth_requestAccounts', params: [] }) setAccount(accounts[0]) } catch (error) { console.error('Failed to connect:', error) } } const signTransaction = async (transaction: any) => { if (!selectedProvider) return try { const result = await selectedProvider.provider.request({ method: 'eth_signTransaction', params: [transaction] }) return result } catch (error) { console.error('Failed to sign transaction:', error) } } return ( <div> {providers.map(provider => ( <button key={provider.info.uuid} onClick={() => setSelectedProvider(provider)} > Connect {provider.info.name} </button> ))} {selectedProvider && ( <button onClick={connectWallet}> Request accounts </button> )} {account && <p>Connected: {account}</p>} </div> ) }

Why dynamic imports are necessary:

  • The library uses Web APIs (window, document, CustomEvent) that do not exist in Node.js
  • Direct imports cause build errors in SSR frameworks (Next.js, Nuxt, SvelteKit, etc.)
  • Dynamic imports ensure the code only runs in the browser environment

Troubleshooting

The eip6963:announceProvider event never fires. Make sure initializeLedgerProvider has been called before dispatching eip6963:requestProvider. The provider announces itself immediately on initialization and again in response to any subsequent eip6963:requestProvider event. If the browser does not support Web HID or Web Bluetooth, the provider is not registered and no announcement is dispatched. Check the Requirements page and confirm the browser console shows no errors from the SDK.

request() rejects with error code -32603 and message “Ledger Provider is busy”. A blocking request (signing or account selection) is already in progress. The provider processes one blocking request at a time. Wait for the current request to complete or be rejected before sending another.

request() rejects with error code 4100 (“Unauthorized”). eth_requestAccounts has not been called yet, or the user disconnected. Call eth_requestAccounts first to prompt the user to select an account.

Next steps

Customize your integration

Explore Configuration options to set up chain support, floating button position, and wallet action buttons.

Review the API

Read the full list of provider methods, events, and error codes in the API reference.

Last updated on
Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Stax, Ledger Flex, Ledger Nano, Ledger Nano S, Ledger OS, Ledger Wallet, [LEDGER] (logo), [L] (logo) are trademarks owned by Ledger SAS.