Wallet verification API
Check any VerifiedWallet certificate for free, or verify your own customers' wallets from your product: proof of ownership by signature on 15+ chains, with identity, AML screening, company checks and multisig on top. Results are signed, so you can check them without trusting us.
Two APIs
- Free check API: no key. Look up a wallet in the public register, fetch a certificate, check our signature, embed a badge. Open to any website.
- Verify API: with a key. Send your customer to our signing page; get a signed result back by webhook and API. Billed per verification, monthly. Typical uses: Travel Rule checks on self-hosted wallets, withdrawals to new addresses, OTC settlement, payroll and payouts, company treasury wallets.
Create a free developer account Test key in a minute. Go live yourself when you're ready, no call needed. Or try it live first.
Free check API
JSON over HTTPS, open to every origin (CORS), no key. Please be reasonable with volume.
GET /api/verify/<wallet address or certificate ID>: certificates in the public register for that wallet.GET /api/cert/<certificate ID>: one certificate, with its signedcoreand ourattestation.GET /api/attester: our public signing key (and any earlier ones). See Public key for how to check a signature.GET /badge/<certificate ID>.svg: a live badge (?style=pill,?theme=dark).GET /api/check/<address>: check before you send (below).GET /api/directory: the business directory, verified company wallets.
Run a wallet checker, a wallet or an exchange? See how to show VerifiedWallet results in your own lookups.
EAS attestations
Every certificate is also available as an Ethereum Attestation Service attestation on Base (off-chain, version 2, EIP-712 signed): GET /api/cert/<id>/eas returns { sig, signer } in the format the EAS SDK and easscan import, plus an easscan link. EVM wallets are the attestation's recipient. Our EAS signer address and schema are published at /api/attester (easSigner, easSchema). Schema: string certificateId, uint8 tier, string network, string wallet, string identity, string aml, string business, uint64 issuedAt, string certificateUrl. Partner API results include certificate.eas.
Check before you send
Call it when your user pastes a withdrawal or payment address. You get a verdict and a sentence you can show as it is:
warning: not verified, but it shares the first and last characters with a verified or listed wallet (lookalikes). This is what address poisoning looks like: stop and ask the user to check.risk: verified, but the latest AML screening was flagged.business: a listed company's verified wallet (directoryhas the registered name, country and number).verified: the owner proved control (and identity, if the certificate includes it).revokedorunknown: no current certificate. Unknown isn't unsafe; the owner just hasn't verified with us.
curl https://verifiedwallet.io/api/check/0x71c7656ec7ab88b098defb751b7401b5f6d8976f
{ "address": "0x71c7…976f", "chain": "evm", "verdict": "business",
"summary": "This is Example Ltd's verified wallet, listed in the VerifiedWallet business directory.",
"directory": { "company": "Example Ltd", "country": "GB", "registrationNumber": "01234567", "certId": "VW-…" },
"lookalikes": [], "certificates": [ … ] }
curl https://verifiedwallet.io/api/verify/0x71c7656ec7ab88b098defb751b7401b5f6d8976f
{ "wallet": "0x71c7…976f",
"certificates": [{ "id": "VW-7K3Q-M2XA", "tier": 2, "chain": "evm", "issuedAt": "…", "kycStatus": "verified", "revoked": false, … }] }
Embed a badge that always shows the certificate's current status:
<a href="https://verifiedwallet.io/cert?id=VW-7K3Q-M2XA">
<img src="https://verifiedwallet.io/badge/VW-7K3Q-M2XA.svg" alt="VerifiedWallet certificate" height="40">
</a>
Verify API: getting started
- Sign up and confirm your email: your
vw_test_…key is waiting. When you're ready, go live in the dashboard (company details, the API terms, a card) and make avw_live_…key. Keep keys on your server; never put them in a web page or app. - Create a verification for your customer. You get a
url. - Send the customer there (redirect or link). They connect their wallet and sign one message (free, nothing moves), then complete identity or company checks if the tier includes them. You cover the cost; they never pay.
- We send you a
verification.completedwebhook, and laterverification.updatedas identity, AML or company results arrive. You can also fetch the verification at any time. - If you set
return_url, the customer gets a button back to your site, with?verification=ver_…added.
Test keys run the real signing flow, but identity, AML and company results are simulated straight away, the certificate is marked test and kept out of the public register, and nothing is billed. Build against test, then switch the key.
Test triggers. With a test key, add "test_outcome" to choose each simulated result, so you can build every path: {"identity": "verified" | "rejected" | "pending", "aml": "clear" | "review" | "flagged" | "pending", "business": "verified" | "rejected" | "pending"}. Anything you leave out passes. Live keys ignore it.
Logs. Your dashboard at dev.verifiedwallet.io shows your last 100 API requests (with any error message) and every webhook delivery, with a button to resend one. We email you at 80% and 100% of your monthly limit.
Authenticate every call with Authorization: Bearer vw_live_… (or vw_test_…). Each key only sees its own mode's verifications.
Create a verification
POST /api/v1/verifications
tier: 1 wallet ownership · 2 + identity (ID document, liveness, face match) · 3 + AML (identity screened against sanctions and PEP lists, wallet screened on-chain) · 4 Business: the company checked against its registry, its key people identity-verified, and the wallet screened. Multisig wallets (Bitcoin, XRP, Tron) and Safes are supported.reference(optional): your own ID for the customer, returned on everything.wallet,chain(optional): require this exact wallet; anything else is refused.return_url(optional): where the customer goes back to. Its site must be on your account's allowed list.business(tier 4, optional):{ "company_name", "country" (2 letters), "registration_number" }, so the customer isn't asked.monitor(tiers 3 and 4): re-screen the wallet every 30 days and alert you if the risk changes.metadata(optional): up to 20 string values, returned as given.
curl https://verifiedwallet.io/api/v1/verifications \
-H "Authorization: Bearer vw_test_…" \
-H "Content-Type: application/json" \
-d '{
"tier": 3,
"reference": "customer-1842",
"return_url": "https://app.example.com/wallets/verified",
"wallet": "0x71c7656ec7ab88b098defb751b7401b5f6d8976f",
"monitor": true
}'
{
"id": "ver_3f9a1c0b7e44d2a9f61c",
"object": "verification",
"mode": "test",
"tier": 3,
"status": "pending",
"reference": "customer-1842",
"url": "https://verifiedwallet.io/start?tier=3&vs=vs_…",
"expiresAt": "2026-10-22T09:00:00.000Z",
…
}
The link lasts 7 days and works once.
Retrieve and list
GET /api/v1/verifications/<id>: the verification and, once issued, the certificate's current results.GET /api/v1/verifications?limit=20: your latest, newest first.GET /api/v1/usage?month=2026-10: billable counts for a month and, with a live key, what the invoice comes to so far.POST /api/v1/verifications/<id>/monitoring{ "enabled": false }: stop monitoring a wallet (ortrueto start it on a completed tier 3 or 4 verification).
{
"id": "ver_3f9a1c0b7e44d2a9f61c",
"status": "completed",
"certificateId": "VW-7K3Q-M2XA",
"certificate": {
"id": "VW-7K3Q-M2XA",
"url": "https://verifiedwallet.io/cert?id=VW-7K3Q-M2XA",
"chain": "evm",
"wallet": "0x71c7656ec7ab88b098defb751b7401b5f6d8976f",
"tier": 3,
"identity": { "status": "verified", "provider": "didit" },
"aml": { "status": "clear", "networks": [{ "network": "ETH", "riskScore": 4, "severity": "LOW" }, …] },
"business": null,
"account": null,
"revoked": false,
"test": false,
"core": { … },
"attestation": { "alg": "ES256", "kid": "ZlARoHYj…", "sig": "…" }
}
}
status is pending until the customer signs, then completed. Identity, AML and company results inside certificate can still be pending for a few minutes after that; each change sends a webhook. Every result carries the certificate's signed core and our ES256 attestation: check them against /api/attester and you don't need to trust our API's word.
Webhooks
We POST JSON to your account's webhook URL: { "id", "type", "created", "mode", "data": { "verification": … } }, with the full verification as above. Events:
verification.completed: the customer signed; the certificate is issued.verification.updated: an identity, AML or company result arrived or changed.certificate.revoked: we revoked the certificate.monitoring.alert: a monitored wallet's AML result changed on re-screening.ping: a test we send when you ask.
Answer 2xx quickly; we retry a few times. Check the VerifiedWallet-Signature header (t=<unix time>,v1=<hex>), an HMAC-SHA256 of "<t>.<raw body>" with your webhook secret:
import crypto from "node:crypto";
// Express-style handler: read the RAW body, not parsed JSON.
export function verifyVerifiedWalletWebhook(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // older than 5 minutes
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Reused identity
If your customer verified their identity with us for the same wallet in the last 12 months, they can choose to reuse that result (after signing with the wallet again) instead of scanning their ID again. You'll see certificate.identity.reused: true and the original verifiedAt. It's billed the same; AML screening always runs fresh.
SDKs
Small, dependency-free clients. Download the file into your project:
- JavaScript (Node 18+, Deno, Bun, Cloudflare Workers): verifiedwallet.mjs
- Python 3.8+: verifiedwallet.py
Both cover verifications, screening, usage, evidence packs, address checks, webhook signature checking and the OAuth flow; the JavaScript one can also check our attestations against our published key.
// JavaScript
import { VerifiedWallet } from "./verifiedwallet.mjs";
const vw = new VerifiedWallet(process.env.VERIFIEDWALLET_KEY);
const v = await vw.verifications.create({ tier: 3, reference: "user-42" });
const event = await VerifiedWallet.webhooks.constructEvent(rawBody, req.headers["verifiedwallet-signature"], process.env.VERIFIEDWALLET_WEBHOOK_SECRET);
# Python
from verifiedwallet import VerifiedWallet, construct_event
vw = VerifiedWallet(os.environ["VERIFIEDWALLET_KEY"])
v = vw.create_verification(tier=3, reference="user-42")
event = construct_event(raw_body, request.headers["VerifiedWallet-Signature"], os.environ["VERIFIEDWALLET_WEBHOOK_SECRET"])
Popup instead of a redirect
Keep your customer on your page: open the verification url (or a Verification Link) in a popup with our embed script, and get told when they've signed and when the page's checks are done. Your site must be one of your return sites (dashboard → Integration). Use the events to update your page, and confirm the result on your server (API or webhook). If the browser blocks popups, it falls back to a normal redirect.
<script src="https://verifiedwallet.io/embed.js"></script>
<script>
document.querySelector("#verify").onclick = async () => {
const { url } = await fetch("/your-server/start-verification", { method: "POST" }).then((r) => r.json());
VerifiedWallet.open(url, {
onSigned: (e) => showSpinner(),
onComplete: (e) => refreshFromServer(e.verificationId),
onClose: () => hideSpinner(),
});
};
</script>
Verify with VerifiedWallet (OAuth / OpenID Connect)
Add a button instead of calling the API. It's standard OAuth 2.0 with PKCE and an OpenID-style id_token, so most sign-in libraries work with it (discovery: /.well-known/openid-configuration). Register your redirect URIs in the dashboard under Integration. Your client ID is your account ID (pt_…); the client secret is any of your API keys (a test key with mode=test). Billed like any verification.
<a href="https://verifiedwallet.io/oauth/authorize?response_type=code
&client_id=pt_…&redirect_uri=https://app.example.com/auth/vw
&scope=openid%20identity%20claims:over_18&state=…&nonce=…
&code_challenge=…&code_challenge_method=S256">
<img src="https://verifiedwallet.io/img/verify-with-vw.svg" alt="Verify with VerifiedWallet" width="260" height="44">
</a>
- Scopes:
wallet(tier 1),identity(tier 2),aml(tier 3),funds(proof of funds),claims:over_18,claims:over_21,claims:country,claims:nationality,claims:not_sanctioned. Addlogin_hint=<wallet>to require a particular wallet. - Back to you:
redirect_uri?code=…&state=…, orerror=access_deniedif they didn't finish. - Token:
POST /oauth/token(form-encoded:grant_type=authorization_code,code,redirect_uri,code_verifier; authenticate with HTTP Basicclient_id:client_secret). You getaccess_token(1 hour), the fullverification, and anid_token(ES256 JWT, keys at /oauth/jwks) withsub=<chain>:<wallet>,wallet,tier,certificate_id,identity_status,aml_statusandclaims. - Later results: an identity check can finish after the redirect. Call
GET /oauth/userinfowith the access token, or listen for the webhook.
Claims: single facts, not documents
Need to know someone is an adult, or where they're from, without holding their ID? Add "claims" (tier 2 and up) and you get just those facts, signed by us: over_18, over_21, country (the country that issued their ID, ISO alpha-3), nationality, not_sanctioned (the person, not the wallet, screened against sanctions and PEP lists). The customer is told which facts you'll see before they start. Never their documents, photos or date of birth.
{ "tier": 2, "claims": ["over_18", "country"] }
// once the identity check passes (verification.updated webhook):
"claims": { "type": "claims", "asOf": "2026-10-14T…",
"values": { "over_18": true, "country": "GBR" }, "attestation": { … } }
Proof of funds
Add "proof_of_funds": true (or { "min_usd": 10000 }) and, when the customer signs, we take a snapshot of what the wallet holds: the native coin plus USDC and USDT, on Ethereum, Base, Arbitrum, Polygon and BNB Chain for EVM wallets, and on Bitcoin, Solana and Tron. You get balances, a dollar total and, with a minimum, meetsMinimum, all signed by us, under funds with a verification.updated webhook. The customer is told before they sign. $0.50 a snapshot.
"funds": { "snapshot": { "type": "proof_of_funds", "wallet": "0x…", "at": "…",
"balances": [{ "network": "ethereum", "asset": "USDC", "amount": "25000", "usd": 25000 }],
"totalUsd": 25000, "minUsd": 10000, "meetsMinimum": true }, "attestation": { … } }
Screen any address
Screen an address with no customer involved, such as a withdrawal destination or the source of a deposit: POST /api/v1/screen with { "address": "…" }. Same AML screening as tier 3 (sanctions, mixers, darknet markets, stolen funds and more), answered straight away and signed. EVM addresses are screened on six networks, others on their own. $0.25 per network. With a test key, an address ending in dead comes back flagged and one ending in beef for review.
curl https://verifiedwallet.io/api/v1/screen -H "Authorization: Bearer vw_live_…" \
-H "Content-Type: application/json" -d '{ "address": "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7" }'
{ "id": "scr_…", "status": "clear", "networks": [{ "network": "TRX", "riskScore": 6, "severity": "LOW", … }], "attestation": { … } }
Evidence packs
For audits and regulators: GET /api/v1/verifications/<id>/evidence returns one file with the verification, the signed certificate, claims and proof of funds, every webhook we sent about it, our public keys, and step-by-step instructions to check the signatures without trusting us. Add ?format=html for a printable page (save it as PDF). Also in your dashboard, next to each verification.
Verification Links (no code)
Not ready to integrate? In your dashboard, make a Verification Link: choose what to check, test or live, and where to send people afterwards. You get a short address (verifiedwallet.io/v/…) and a QR code to share by email, chat or print. Each person who opens it gets their own verification, billed the same as one made through the API. The dashboard counts how many started and finished per link; webhooks arrive as usual, with the link's linkId and a reference of link:<code>. Turn a link off at any time.
Pricing
Pay as you go, no setup fee, no minimum. Test keys are free. Invoiced monthly in US dollars for the previous month's live usage, payable by card or bank transfer within 14 days.
| Per completed verification | Price |
|---|---|
| Wallet ownership (tier 1) | $0.50 |
| + Identity (tier 2) | $2.00 |
| + AML screening (tier 3) | $3.50 |
| Business: company + up to 3 key people (tier 4) | $25.00 |
| Each extra key person | $1.00 |
| Monitoring, per wallet per month (re-screens included) | $1.00 |
| Proof of funds snapshot | $0.50 |
| Address screening, per network screened | $0.25 |
| Claims (over 18, country, nationality, sanctions) | Free |
| Evidence packs, Verification Links, address checks | Free |
Growth: $299 a month including $400 of usage, plus a named contact. Switch plans yourself in the dashboard: upgrades apply straight away, downgrades from the next month. Enterprise: volume pricing, an uptime commitment and a signed data processing agreement; talk to us.
Create a free developer account
Errors and limits
Errors are JSON { "error": "…" } with a plain-English message: 400 bad request, 401 missing or wrong key, 404 not found, 409 already used, 429 slow down. The API is versioned (/api/v1); we add fields but never remove or rename them within a version. Machine-readable spec: openapi.json.
Try it live
This starts a real test verification on our demo account, exactly as your server would, and shows what your server would get back. Sign with any wallet you have; the identity, AML and company results are simulated.