Appearance
Result
Every successful start / verify / complete returns a WalletVerification. Branch UI on status. The same customerId + address pair is reused - a verified pair short-circuits the next create.
On verified, persist verificationId, address, and verifiedAt on your backend (or rely on the webhook). The returned record is enough to update UI; treat the record and/or webhook as the source of truth for fulfillment.
Statuses
| Status | Terminal? | What to do |
|---|---|---|
pending | no | Keep going / retry the current step (connect, sign, or Coinbase) |
verified | yes | Success. Stop waiting |
failed | yes | Show an error. Start a new verify() / startVerification() |
expired | yes | Challenge TTL (~15 minutes) exceeded. Start a new verification |
An invalid signature does not move the record to failed. The call errors and status stays pending - call verify() again with the same customer / address / network to re-sign.
failed and expired need a new verification. Calling verify() / startVerification() again is enough - the server issues a new id.
For Coinbase redirect, ?status= on the landing URL is a hint. Always use the record returned by completeVerification().
WalletVerification
| Field | Meaning |
|---|---|
verificationId | Server id. Use it if you persist the run or read ?verificationId= after Coinbase redirect |
status | Where the run is: pending (keep going), verified (done), failed / expired (start a new one) |
method | How they proved control: 'signature' (wallet) or 'exchange_oauth' (Coinbase). 'satoshi' is reserved |
network | Chain family of address: 'evm', 'solana', or 'bitcoin' |
address | Address the user claimed to control |
orderId | Merchant order reference, or null when omitted on create |
verifiedAt | ISO timestamp once status is verified; otherwise null |
challenge | Pending proof payload, or null when the record is terminal |
ts
if (verification.status === 'verified') {
verification.verificationId
verification.address
verification.verifiedAt
}challenge
Present while status is pending. null after the challenge is consumed or the TTL lapses.
| Field | Meaning |
|---|---|
message | Exact string the wallet must sign (signature only). Sign this as returned |
authorizationUrl | Coinbase login URL (exchange_oauth only). Expires with the challenge |
nonce | Server nonce bound into the challenge |
expiresAt | When this challenge dies. After this, status becomes expired |
You do not normally read challenge yourself. verify() signs message; openVerificationTab() / redirect use authorizationUrl.
Identity and reuse
customerId + address is the cache key. If that pair is already verified, the next verify() or startVerification() returns status: 'verified' and challenge: null. Show success and skip the wallet or Coinbase UI.
Webhooks
Pass webhookUrl on verify() / startVerification() only when you need to override the partner default. The webhook is server-to-server - the SDK does not deliver it to the page. Treat the returned WalletVerification (and your webhook) as the source of truth for fulfillment.