Skip to main content
The wallet-ownership widget is a hosted page that proves one of your customers controls a specific crypto wallet address. Under the EU TFR, a VASP must verify ownership of a self-hosted (unhosted) wallet before releasing funds to or from it above the sunrise threshold - the widget is how Didit collects that proof without you building signing flows yourself. A verified proof marks the matching wallet address book entry as is_ownership_verified: true, which lets a linked Travel Rule transfer advance out of UNCONFIRMED_OWNERSHIP.

When Didit mints it for you

You rarely need to mint a session yourself. When auto_wallet_verification is on in your Travel Rule settings (the default), Didit auto-mints a widget session at transaction creation whenever a transfer needs end-user proof of wallet control:
  • The transfer is in UNCONFIRMED_OWNERSHIP - the counterparty wallet was declared self-hosted at submission, or an inbound transfer hit an unverified address-book entry.
  • The transfer is outbound and in COUNTERPARTY_VASP_NOT_FOUND or ON_HOLD - no counterparty VASP could be reached, so ownership proof is the remaining path to completion.
The auto-minted session is returned in the transaction’s action_required block (type: "wallet_ownership", with the url to open), and the SDKs launch it automatically on client-side submission. Mint manually with POST /v3/travel-rule/widget-session/ when you want to pre-verify a wallet before any transfer exists, or when you have auto_wallet_verification turned off.

What it proves

The widget offers up to four proof methods. Which ones appear depends on how you minted the session and your Travel Rule settings:
MethodHow it worksWhen it’s offered
MESSAGE_SIGNINGThe customer signs a Didit-issued challenge string with their wallet. Supports EVM, Solana, Bitcoin, and Tron wallets; Didit verifies the signature against the address, and signing with a different account than the one being verified is rejected with a clear switch-accounts prompt. Installed browser wallets are discovered through the EIP-6963 and Wallet Standard protocols, so every extension appears as its own signing option, and mobile wallets connect through WalletConnect.Always, for supported chain families.
SATOSHI_TESTThe customer sends a tiny on-chain amount from the wallet to a deposit address you control; Didit’s watcher confirms the sender, amount tolerance, and confirmation depth, then automatically completes the proof once the deposit confirms - see Satoshi test verify contract below.Only when you pass a satoshi_deposit_address at mint time.
SCREENSHOTThe customer uploads a screenshot of the wallet showing the address (JPEG/PNG/WebP, up to 10 MB). Unlike the other methods it never auto-verifies: your compliance team reviews it and confirms from the transaction’s Travel Rule tab in the console, or via POST .../travel-rule/ownership/.Only when allow_screenshot_proof is true in your settings.
SELF_DECLARATIONThe customer attests ownership without cryptographic proof - a last resort where the jurisdiction tolerates it.Only when allow_self_declaration is true in your settings.

Satoshi test verify contract

Verifying a Satoshi test deposit is asynchronous, because the transaction has to be seen on-chain and then reach enough confirmations. Submitting the deposit’s txid returns 200 with status: "PENDING" the moment the deposit is seen on-chain but before it has enough confirmations - the txid is retained, not discarded. It returns 200 with status: "VERIFIED" once the deposit confirms. It returns 400 only for a genuinely wrong deposit: absent, sent from an unexpected address, or the wrong amount. It returns 410 if the test window expires before a valid deposit is seen. A background job automatically rechecks pending deposits and completes the proof the moment the transaction confirms, so the customer is never stranded after sending funds - they do not need to resubmit the txid. When SATOSHI_TEST is offered, the widget session’s own expires_at is extended to cover the Satoshi test window, giving the deposit time to confirm before the session itself expires.

Flow

1

Mint a widget session

Call POST /v3/travel-rule/widget-session/ with the wallet you need verified. Optionally link it to a transfer so the transfer advances automatically once ownership is proven.
2

Open the returned URL

Send your customer to the url from the response. They complete a proof there - no Didit account or API key needed; the token in the URL is the authentication.
3

Detect completion

When a proof is verified, the widget marks the address-book entry verified and, if the session is linked to a transfer, advances it. Watch for travel_rule.status.updated (linked transfers) or poll the widget context.

Mint a widget session

curl -X POST https://verification.didit.me/v3/travel-rule/widget-session/ \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "wallet_address": "0xBeneficiaryWallet01",
    "chain": "ethereum",
    "holder_name": "Ana Diaz",
    "vendor_data": "user-042",
    "transaction_id": "22222222-3333-4444-5555-666666666666",
    "satoshi_deposit_address": "",
    "callback_url": "https://yourapp.example/wallet-verified",
    "expires_in_minutes": 60
  }'
FieldTypeDescription
wallet_addressstringThe wallet address to verify.
chainstringChain identifier for the address (for example ethereum, bitcoin, solana).
holder_namestringName of the wallet holder, used for the beneficiary name match.
vendor_datastringYour internal identifier for the holder, carried onto the created address-book entry.
transaction_idstring (uuid)Optional. Didit transaction UUID of a Travel Rule transfer to advance when the proof verifies. Omit to verify a wallet standalone.
satoshi_deposit_addressstringOptional. A deposit address you control; supplying it enables the SATOSHI_TEST method.
callback_urlstringOptional. Where the widget returns the customer after completion.
expires_in_minutesintegerOptional. Lifetime of the minted link; automatically extended to cover the Satoshi test window when SATOSHI_TEST is offered.
{
  "widget_session_id": "77777777-8888-9999-aaaa-bbbbbbbbbbbb",
  "token": "3s9x...urlsafe",
  "url": "https://verify.didit.me/wallet-ownership/3s9x...urlsafe",
  "expires_at": "2026-07-06T13:00:00Z"
}
Always open the url returned in the response verbatim - do not construct it yourself.

Open the returned URL

The widget is web-only in this release. Open the url in the system browser or a custom tab - never inside a Didit mobile SDK webview.Wallet proofs rely on deep links that hand off to the customer’s wallet app (WalletConnect, Phantom, a Bitcoin wallet) and back. Those deep links cannot escape an in-app webview, so a widget opened inside the Didit Web/Flutter/React Native/iOS/Android SDK webview will dead-end when the wallet tries to return.On mobile, launch the url with the OS browser (SFSafariViewController / Chrome Custom Tabs, or the default browser) instead of rendering it in a webview. On web, a normal tab or popup is fine.

Detect completion

The widget exposes a public read endpoint keyed by the token - poll it to see progress, or rely on webhooks when the session is linked to a transfer:
curl https://verification.didit.me/v3/travel-rule/widget/{token}/
{
  "vasp_name": "Origin CASP SL",
  "wallet_address": "0xBeneficiaryWallet01",
  "chain": "ethereum",
  "holder_name": "Ana Diaz",
  "allowed_methods": ["MESSAGE_SIGNING", "SATOSHI_TEST", "SCREENSHOT"],
  "transfer_status": "UNCONFIRMED_OWNERSHIP",
  "callback_url": "https://yourapp.example/wallet-verified",
  "expires_at": "2026-07-06T14:00:00Z",
  "completed_at": null,
  "proofs": [
    {
      "proof_id": "88888888-9999-aaaa-bbbb-cccccccccccc",
      "method": "SATOSHI_TEST",
      "status": "PENDING",
      "wallet_address": "0xBeneficiaryWallet01",
      "chain": "ethereum",
      "deposit_address": "0xYourDepositWallet",
      "expected_amount": "0.00015",
      "expires_at": "2026-07-06T14:00:00Z",
      "created_at": "2026-07-06T13:05:00Z"
    }
  ]
}
  • completed_at becomes non-null once a proof is verified.
  • Each entry in proofs carries the fields its method needs: a SATOSHI_TEST proof adds deposit_address, expected_amount, and its own expires_at; a MESSAGE_SIGNING proof adds challenge, the string the customer signs.
  • A SATOSHI_TEST proof can sit at status: "PENDING" while its deposit is seen on-chain but unconfirmed, with no action needed from you or the customer - see Satoshi test verify contract above.
  • When the session was minted with a transaction_id, verifying the proof advances the linked transfer (out of UNCONFIRMED_OWNERSHIP, or resolving an inbound TRP transfer), and a travel_rule.status.updated webhook fires. Prefer the webhook over polling.
  • The context endpoint returns 404 for an unknown token and 410 once the session has expired without completing.
There are no one-shot dependencies here: the widget and your backend both converge on the same durable state (the verified address-book entry and the transfer status), so a dropped webhook or a closed tab is always recoverable by re-reading the transfer or the widget context.

Localization and branding

The widget renders in 54 languages, following the language path of the link (or the browser default). When whitelabel is enabled for your application, the widget inherits your verification-flow branding automatically: logo, brand colors, button and panel styling, custom font, favicon, and your app’s public name in the title. No separate configuration - the same customization you set up for KYC sessions applies here. A user who returns to an already-completed link sees the completion screen again; if you passed a callback_url, the widget returns them there. Expired links show a branded expiry screen prompting them to request a new link (mint a new session or use action: "resend" flows as appropriate).

Next steps

Travel Rule

The full exchange lifecycle, rails, settings, and receiving flows.

Wallet address book

Register the wallets you control so inbound transfers resolve against them.

Webhooks

Subscribe to travel_rule.status.updated to react when a proof advances a transfer.

Native SDKs

Open the widget URL with the system browser, not the SDK webview.