Skip to content

Verify wallet ​

client.wallets.verify() is the one-shot signature path: create a verification, connect the wallet, sign the server challenge, and wait until the record is terminal.

For a QR-first picker (show a QR, then optionally switch to the extension), use prepareVerify() then verify({ prepared }) or verify({ verificationId }). That reuses the pending record instead of creating a second one.

Prefer these over calling connect() and signMessage() yourself. The SDK signs challenge.message exactly as returned - do not build SIWE or rewrite the text.

Coinbase OAuth is a different module: Coinbase. Walkthrough: Wallets example.

Bring your own QR component - qrcode.react in the examples is only an example.

When is address required? ​

Pathaddress
Injected / WalletConnect / popup verify()Optional - omit to verify the connected account
prepareVerify() / browse deeplinkRequired
CoinbaseRequired - see Coinbase

If you pass address and the connected wallet is a different account, verify() throws WALLET_VERIFICATION_ADDRESS_MISMATCH.

prepareVerify ​

QR-first start. address is required. Always POSTs a pending record, then starts WalletConnect pairing or a browse deeplink. Resolves when the QR is ready (or immediately if that customerId + address is already verified).

Skip transport - prepare prefers a QR when the wallet can show one (browse-deeplink wallet → deeplinkUrl, else WalletConnect). Injected is not the prepare default.

ts
const prepared = await client.wallets.prepareVerify({
  network: 'evm',
  customerId: 'user-1234',
  provider: IntegrationProvider.MetaMask,
  address: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B',
  onUri: ({ uri, deeplinkUrl }) => {
    // WalletConnect QR / open-in-app
  },
  onDeeplinkUrl: ({ url }) => {
    // browse QR
  },
  onVerified: finish,
  onError: showError,
})

prepared.pairingUri
prepared.pairingDeeplinkUrl
prepared.deeplinkUrl
prepared.verification.verificationId

const verification = await prepared.wait()

Use callbacks, wait(), or both - they share one completion. Do not wait() and also verify({ prepared }) for the same completion.

Switch to the extension while the QR is up (same pending record):

ts
await client.wallets.verify({
  prepared,
  transport: 'injected',
  provider: IntegrationProvider.MetaMask,
  network: 'evm',
  customerId: 'user-1234',
})

Or continue later with just the id:

ts
await client.wallets.verify({
  verificationId: prepared.verification.verificationId,
  provider: IntegrationProvider.MetaMask,
  network: 'evm',
  customerId: 'user-1234',
  transport: 'injected',
})

prepared.cancel() aborts pairing / the deeplink wait (WALLET_VERIFICATION_CANCELLED). verify({ prepared }) takes over that session and does not throw WALLET_VERIFICATION_IN_PROGRESS.

verify ​

ts
const verification = await client.wallets.verify({
  network: 'evm',
  customerId: 'user-1234',
  provider: IntegrationProvider.MetaMask,
})
// verification.status === 'verified'

VerifyWalletSignatureRequest:

FieldMeaning
network'evm' | 'solana' | 'bitcoin'. Signature today: evm and solana
customerIdYour id for the end user (account id, uuid - not a Swapped id). Combined with address, this is the reuse key
providerWallet the user picked from getAvailable
address?Address to verify. Omit on injected / WalletConnect / popup to use the connected account for network. Required for browse-deeplink wallets
walletId?Reuse an existing gateway connection instead of pairing again
orderId?Merchant order reference (metadata only)
customerEmail?Optional end-user email. Forwarded to the API on create
webhookUrl?Optional override of the partner default webhook
transport?'injected' or 'walletconnect'. Default: installed extension → injected, else WalletConnect (browse-deeplink wallets that are not installed take the deeplink path)
chainId?EVM only: chain to request at connect time
onUri?WalletConnect pairing callback { uri, deeplinkUrl, provider }
onDeeplinkUrl?Browse URL that opens verification inside a wallet in-app browser { url, provider }
force?WalletConnect only. Cancel any in-flight pairing before starting a new one
forcePopup?Always run this injected connect / sign in a popup
popupIfUnavailable?If the host cannot see this extension, open a popup instead of failing
signal?Abort the wait after a deeplink QR is shown (modal close). Throws WALLET_VERIFICATION_CANCELLED
prepared?Finish a prepareVerify session (same pending record; cancels pairing)
verificationId?GET that record, then connect / sign / proof

verify() returns a WalletVerification. If that customerId + address is already verified, it returns status: 'verified' without prompting the wallet.

Only one verification may run on this client at a time (WALLET_VERIFICATION_IN_PROGRESS).

What the user sees ​

When transport is omitted, the SDK picks injected if the extension is installed, otherwise WalletConnect or a browse deeplink.

SituationUI
Extension installedWallet connect + sign prompts
Desktop, no extension, WalletConnect supportedQR from onUri (uri to render, deeplinkUrl to open the app on this device)
Mobile in-app browserInjected
Browse-deeplink wallet (Phantom, Coinbase Wallet, MetaMask iOS, Bitget iOS)onDeeplinkUrl QR on desktop; the SDK opens the wallet on mobile. Pass address
Popup wallet (for example Trust injected on desktop)A popup for connect and sign. Allow popups

WalletConnect QR still arrives as onUri even when a popup is involved.

Injected ​

ts
const verification = await client.wallets.verify({
  network: 'evm',
  customerId: 'user-1234',
  provider: IntegrationProvider.MetaMask,
  transport: 'injected',
  popupIfUnavailable: true,
})

Omit address to verify the connected account. If you pass address and the connected wallet is a different account, verify() throws WALLET_VERIFICATION_ADDRESS_MISMATCH and does not submit proof.

forcePopup and popupIfUnavailable apply to injected verify. Wallets with isPopupConnection: true always complete connect and sign in a popup.

Popup failures: WALLET_POPUP_BLOCKED, WALLET_POPUP_CLOSED, WALLET_POPUP_IN_PROGRESS, WALLET_POPUP_NO_OPENER.

WalletConnect ​

Pass onUri (or both uri and deeplinkUrl from it) so you can render a QR and an “Open in wallet” button.

ts
const verification = await client.wallets.verify({
  network: 'evm',
  customerId: 'user-1234',
  provider: IntegrationProvider.Trust,
  transport: 'walletconnect',
  force: true,
  onUri: ({ uri, deeplinkUrl }) => {
    // `uri` - QR / clipboard
    // `deeplinkUrl` - open the wallet app on this device
  },
})

cancelPairing() aborts the in-flight pairing and clears the URI. The pending verify() rejects with WALLET_PAIRING_CANCELLED.

ts
const cancelled = await client.wallets.cancelPairing()

Only one WalletConnect pairing - and one QR - can be in flight. A second WalletConnect verify() without force: true throws WALLET_PAIRING_IN_PROGRESS.

Some wallets on some devices do not use WalletConnect. requiresDeepLink(provider) is true (also on AvailableWallet.isDeepLinkConnection).

address is required. The SDK creates the verification, calls onDeeplinkUrl with a URL you can show as a QR, opens the wallet on mobile, and waits on this page until the user finishes in the wallet browser.

ts
const abort = new AbortController()

const verification = await client.wallets.verify({
  address: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B',
  network: 'evm',
  customerId: 'user-1234',
  provider: IntegrationProvider.Phantom,
  signal: abort.signal,
  onDeeplinkUrl: ({ url }) => {
    // desktop: render `url` as a camera QR
  },
})

// modal close
abort.abort()

Aborting signal throws WALLET_VERIFICATION_CANCELLED. Closing the picker without aborting leaves the wait running.

Do not use Connect getDeepLinkUrl / openDeepLink - those are session checkout URLs.

Optional connect / connections ​

verify() connects on its own when needed. Call connect() first if you want an explicit connect step, then pass walletId into verify().

ts
const connection = await client.wallets.connect({
  provider: IntegrationProvider.MetaMask,
  transport: 'injected',
})

const verification = await client.wallets.verify({
  network: 'evm',
  customerId: 'user-1234',
  provider: IntegrationProvider.MetaMask,
  walletId: connection?.walletId,
})

ConnectWalletRequest fields (transport, onUri, force, forcePopup, popupIfUnavailable, chainId) match Connect connect. namespaces is not on verify() - the host always requests the namespace for network.

MethodWhat it does
prepareVerify(request)Create a pending record and start the QR. address required. Then wait() or verify({ prepared })
connect(request)Connect the wallet without verifying. Pass walletId into verify() afterward. null when this device only opens the wallet app
getConnections()Already-approved injected / WalletConnect sessions on this page - reuse with walletId
cancelPairing()Drop the current WalletConnect QR. The pending verify() rejects with WALLET_PAIRING_CANCELLED
ts
const connections = await client.wallets.getConnections()

Retry ​

OutcomeWhat to do
Invalid signatureStatus stays pending. Call verify() again with the same customerId / address / network to re-sign that record
failed or expiredCall verify() again. The server issues a new verification
User rejected connectWALLET_CONNECT_REJECTED - offer retry
User rejected signWALLET_SIGNATURE_REJECTED - offer retry
Already verified pairstatus: 'verified' immediately

See Result for statuses and Errors for codes.

Advanced ​

signMessage({ walletId, message, namespace? }) signs an arbitrary string through the gateway. You do not need it for verification - verify() signs the challenge for you.