Appearance
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?
| Path | address |
|---|---|
Injected / WalletConnect / popup verify() | Optional - omit to verify the connected account |
prepareVerify() / browse deeplink | Required |
| Coinbase | Required - 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:
| Field | Meaning |
|---|---|
network | 'evm' | 'solana' | 'bitcoin'. Signature today: evm and solana |
customerId | Your id for the end user (account id, uuid - not a Swapped id). Combined with address, this is the reuse key |
provider | Wallet 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.
| Situation | UI |
|---|---|
| Extension installed | Wallet connect + sign prompts |
| Desktop, no extension, WalletConnect supported | QR from onUri (uri to render, deeplinkUrl to open the app on this device) |
| Mobile in-app browser | Injected |
| 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.
Open in wallet app (deeplink)
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.
| Method | What 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
| Outcome | What to do |
|---|---|
| Invalid signature | Status stays pending. Call verify() again with the same customerId / address / network to re-sign that record |
failed or expired | Call verify() again. The server issues a new verification |
| User rejected connect | WALLET_CONNECT_REJECTED - offer retry |
| User rejected sign | WALLET_SIGNATURE_REJECTED - offer retry |
| Already verified pair | status: '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.