For dApps & ServicesLedger 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 registers the provider with the EIP-6963 discovery protocol. It returns a cleanup function — call it when your application unmounts to remove the UI and deregister event listeners.

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.

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.