API Integration
Build custom self-hosted verification flows with the REST API
The Wallet Verification Widget is the fastest way to go live, but you can also integrate directly via the API. Use the API when you want to build your own UI, support native mobile flows, or control each verification step from your backend.
Direct API integration follows the same pattern for every verification method:
- Create a wallet verification request.
- Submit proof of ownership for the selected flow.
- Read the updated status from the API or your webhook notification.
1. Create a Verification
Create the wallet verification before collecting proof from the wallet owner.
curl --location 'https://api-dev.cryptoswift.eu/wallet-verification' \
--header 'x-api-key: $API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"asset": "ETH",
"blockchain": "Ethereum",
"address": "0x32Be343B94f860124dC4fEe278FDCBD38C102D88",
"metadata": "customer-123",
"allowedFlows": ["SIGNATURE_PROOF", "VISUAL_PROOF", "SELF_DECLARED"],
"origin": "https://app.example.com"
}'
Mandatory fields:
asset: The virtual asset, for exampleETHorBTCblockchain: The blockchain name or supported EVM chain IDaddress: Self-hosted wallet address to verify
Optional fields:
metadata: Your own wallet, user, or case referenceallowedFlows: Restrict which proof methods can be usedorigin: Required when validating browser-origin-bound signature messagessatoshiFlow: Required when you allowSATOSHI_TEST; includedepositAddressand optionalamountreuse: Reusable verification matching input and policy
Example response:
{
"id": "9f3dc458-a2be-4a34-bcb7-f1f677a0864c",
"token": "97857f6c-396a-4d68-a1bf-563bfb76c5b6",
"url": "https://wallet-dev.cryptoswift.eu/?token=97857f6c-396a-4d68-a1bf-563bfb76c5b6",
"asset": "ETH",
"blockchain": "Ethereum",
"address": "0x32Be343B94f860124dC4fEe278FDCBD38C102D88",
"metadata": "customer-123",
"status": "PENDING",
"flow": null,
"allowedFlows": ["SIGNATURE_PROOF", "VISUAL_PROOF", "SELF_DECLARED"],
"reuse": null,
"createdAt": "2026-05-15T09:00:00.000Z"
}
Use id for all later status checks, proof submissions, manual updates, and webhook reconciliation.
Reusable Verification
Reusable verification can automatically complete a new verification by matching it with a previous eligible verified wallet verification for the same address and blockchain. Reuse matching is cross-tenant.
For a full explanation of matching policies, flow, and storage, see Reusable Wallet Verification.
curl --location 'https://api-dev.cryptoswift.eu/wallet-verification' \
--header 'x-api-key: $API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"asset": "ETH",
"blockchain": "Ethereum",
"address": "0x32Be343B94f860124dC4fEe278FDCBD38C102D88",
"allowedFlows": ["SIGNATURE_PROOF"],
"reuse": {
"subject": {
"type": "NATURAL",
"nameHash": "6f1c9d3d5c2f7b1e8a4f7c9b0d3a1e5f6c8b9a0d2e4f6a8b0c1d3e5f7a9b1c3"
},
"policy": {
"minimumMatchLevel": "MEDIUM",
"maxReuseAgeDays": 180
}
}
}'
nameHash is required for reusable verification. Hash the owner's full name after NFKC normalization, trimming, lowercasing, and collapsing repeated whitespace to one space. Send the lowercase 64-character SHA-256 hex digest, not the plain-text name.
Matching levels:
LOW: Address, blockchain, and subject type matchMEDIUM: Address, blockchain, and name hash matchHIGH: Address, blockchain, subject type, and name hash match
If a reusable match is found, the response has status: "VERIFIED" and reuse contains the applied match level, matched attributes, and policy. If no match is found, the response remains PENDING and you continue with proof submission.
2. Submit Proof of Ownership
Submit proof to the existing verification:
POST {{apiBaseUrl}}/wallet-verification/{id}/verification
This endpoint does not create a wallet verification. It operates on the existing verification identified by {id}. The verificationFlow value selects and initiates the method to use:
| Verification flow | Primary request format | What submission does |
|---|---|---|
SIGNATURE_PROOF | application/json | Verifies the submitted message and signature against the wallet address. A valid proof completes the flow with VERIFIED. |
SELF_DECLARED | application/json | Records the wallet owner's declaration and immediately completes the flow with VERIFIED. |
SATOSHI_TEST | application/json | Starts monitoring for the configured transaction. The verification remains PENDING until CryptoSwift detects a match. |
VISUAL_PROOF | multipart/form-data | Uploads evidence and sets the verification to ACTION_REQUIRED for manual review. |
Multipart is also supported for SIGNATURE_PROOF, SELF_DECLARED, and SATOSHI_TEST, but it is not required. VISUAL_PROOF requires multipart because it uploads files.
The API reference may describe this shared endpoint as multipart/form-data because VISUAL_PROOF accepts file uploads. That endpoint-level annotation does not mean JSON is unsupported for the non-file flows listed above.
Authenticate with the X-Api-Key header, following the API authentication instructions. Collect the signature in the user's wallet, but submit it to CryptoSwift from your backend. Never expose an API key in browser or mobile code.
Cryptographic Signature Proof
Cryptographic signature proofs verify that the user controls the wallet by signing an off-chain message with the wallet's private key.
For SIGNATURE_PROOF, submit these request fields:
verificationFlow:SIGNATURE_PROOFmessage: The exact, non-empty plain-text message signed by the walletsignature: The resulting cryptographic signature
CryptoSwift verifies the signature and the address associated with the wallet-verification request. A successful proof sets the verification status to VERIFIED.
EVM Signature Scheme
For Ethereum and other supported EVM wallets, CryptoSwift expects an EIP-191 plain-message signature. Generate it with a compatible wallet interface, such as:
personal_sign- viem
walletClient.signMessage - ethers
signer.signMessage
CryptoSwift recovers the signer address from the submitted message and signature. The recovered address must match the wallet address associated with the wallet-verification request.
The SIGNATURE_PROOF flow does not accept arbitrary EIP-712 typed-data signatures. Do not submit signatures created with signTypedData. This includes EIP-3009 receiveWithAuthorization signatures: they authorize a token transfer, but cannot be used as CryptoSwift wallet-ownership proof.
The message content can be customer-defined, but the signature scheme cannot. An EVM signature must still be an EIP-191 plain-message signature.
Message Requirements
For direct API submission, CryptoSwift does not prescribe a message-content format. The minimum accepted content is a non-empty string, provided that the signature was generated over exactly that string.
For replay resistance and auditability, strongly prefer a unique challenge that contains:
- The purpose of the signature
- The wallet-verification ID
- A cryptographically random nonce
- An issued-at timestamp
- Optionally, an expiry timestamp and the requesting service or domain
Recommended format:
CryptoSwift wallet ownership verification | verificationId: <verification-id> | nonce: <cryptographically-random-nonce> | issuedAt: <ISO-8601-timestamp>
The nonce makes each challenge unique, while the verification ID binds it to a specific CryptoSwift verification. Together, they reduce the risk that a valid signature can be replayed for another request. If your service adds an expiry timestamp, enforce it before submitting the proof.
Follow these rules:
- Submit the exact original message that was signed.
- Preserve capitalization, spaces, line breaks, punctuation, and every other character.
- Submit the original plain-text message, not its hash.
- Do not manually add the Ethereum signed-message prefix when using
signMessageorpersonal_sign; the wallet adds the EIP-191 prefix.
Direct API Versus Widget Flow
| Integration | Message-content validation | Cryptographic validation |
|---|---|---|
| Direct API submission | You generate any non-empty message. No CryptoSwift-specific content format is enforced. | CryptoSwift validates the chain-specific signature and checks that the recovered address matches the wallet-verification request. |
| CryptoSwift-hosted widget | Additional validation applies, including the expected domain, CAIP-2 chain ID, and an issued-at timestamp within the accepted time window. | CryptoSwift validates the chain-specific signature and wallet address. |
Sign the Challenge with viem
This viem v2 browser example uses the wallet's injected EIP-1193 provider. Replace <verification-id> with the ID returned when you created the verification.
import { createWalletClient, custom } from 'viem';
const walletClient = createWalletClient({
transport: custom(window.ethereum)
});
const [account] = await walletClient.requestAddresses();
const verificationId = '<verification-id>';
const nonce = crypto.randomUUID();
const issuedAt = new Date().toISOString();
const message =
`CryptoSwift wallet ownership verification | ` +
`verificationId: ${verificationId} | ` +
`nonce: ${nonce} | issuedAt: ${issuedAt}`;
const signature = await walletClient.signMessage({
account,
message
});
// Send the original `message` and `signature` to your backend.
console.log({ account, message, signature });
Sign the Challenge with ethers
This example uses the ethers v6 BrowserProvider and its signer.signMessage method.
import { BrowserProvider } from 'ethers';
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const account = await signer.getAddress();
const verificationId = '<verification-id>';
const nonce = crypto.randomUUID();
const issuedAt = new Date().toISOString();
const message =
`CryptoSwift wallet ownership verification | ` +
`verificationId: ${verificationId} | ` +
`nonce: ${nonce} | issuedAt: ${issuedAt}`;
const signature = await signer.signMessage(message);
// Send the original `message` and `signature` to your backend.
console.log({ account, message, signature });
Sign the Challenge with a Browser Provider
When calling personal_sign directly, encode the message as UTF-8 hexadecimal for the provider. Keep the original plain-text message for submission to CryptoSwift.
function utf8ToHex(value) {
const bytes = new TextEncoder().encode(value);
return `0x${Array.from(bytes, (byte) =>
byte.toString(16).padStart(2, '0')
).join('')}`;
}
const [account] = await window.ethereum.request({
method: 'eth_requestAccounts'
});
const verificationId = '<verification-id>';
const nonce = crypto.randomUUID();
const issuedAt = new Date().toISOString();
const message =
`CryptoSwift wallet ownership verification | ` +
`verificationId: ${verificationId} | ` +
`nonce: ${nonce} | issuedAt: ${issuedAt}`;
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [utf8ToHex(message), account]
});
// Submit `message`, not utf8ToHex(message), to CryptoSwift.
console.log({ account, message, signature });
Submit the Signature as JSON with curl
Send the proof from your backend as application/json. Use the exact message and signature returned by the signing step.
API_KEY='<api-key>'
VERIFICATION_ID='<verification-id>'
curl --location --request POST \
'https://api-dev.cryptoswift.eu/wallet-verification/'"$VERIFICATION_ID"'/verification' \
--header "X-Api-Key: $API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"verificationFlow": "SIGNATURE_PROOF",
"message": "CryptoSwift wallet ownership verification | verificationId: <verification-id> | nonce: <cryptographically-random-nonce> | issuedAt: <ISO-8601-timestamp>",
"signature": "<0x-prefixed-signature>"
}'
Submit the Signature as JSON with JavaScript
The following example uses the built-in fetch API available in current Node.js runtimes. Run it on your server, where the API key remains private.
const apiBaseUrl = '<api-base-url>';
const apiKey = '<api-key>';
const verificationId = '<verification-id>';
const message =
'CryptoSwift wallet ownership verification | ' +
'verificationId: <verification-id> | ' +
'nonce: <cryptographically-random-nonce> | ' +
'issuedAt: <ISO-8601-timestamp>';
const signature = '<0x-prefixed-signature>';
const response = await fetch(
`${apiBaseUrl}/wallet-verification/${verificationId}/verification`,
{
method: 'POST',
headers: {
'X-Api-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
verificationFlow: 'SIGNATURE_PROOF',
message,
signature
})
}
);
if (!response.ok) {
throw new Error(
`Signature proof failed (${response.status}): ${await response.text()}`
);
}
const verification = await response.json();
console.log(verification);
Troubleshooting
| Problem | What to check |
|---|---|
| Empty message or signature | Both message and signature are required. The message must contain at least one character. |
| Invalid or non-hex EVM signature | Submit the complete signature returned by the wallet, including its 0x prefix. Use an EIP-191 message-signing method supported by the wallet. |
| Address mismatch | Confirm that the account used to sign is the same address stored on the wallet-verification request. CryptoSwift rejects a proof when the recovered signer differs. |
| Message changed after signing | Submit the exact original string. Check capitalization, whitespace, line breaks, punctuation, and text encoding. |
| EIP-712 typed-data signature | Sign again with personal_sign, viem signMessage, or ethers signMessage. signTypedData and EIP-3009 authorization signatures are not valid ownership proofs. |
| Incorrect request format | Prefer JSON for SIGNATURE_PROOF, SELF_DECLARED, or SATOSHI_TEST; multipart is also accepted for those flows. Use multipart for VISUAL_PROOF, because JSON cannot carry the uploaded proof files. |
| Verification already completed or expired | Retrieve the verification by ID and check its current status. If it has expired, create a new verification and challenge instead of reusing the old proof. |
Micro Transactions
A Satoshi Test verifies ownership by asking the wallet owner to send a specific amount of native asset from the self-hosted wallet to your deposit address.
Please ensure you set up a webhook before initiating a Satoshi Test verification.
Create the verification with SATOSHI_TEST allowed and provide satoshiFlow.depositAddress.
curl --location 'https://api-dev.cryptoswift.eu/wallet-verification' \
--header 'x-api-key: $API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"asset": "BTC",
"blockchain": "Bitcoin",
"address": "BsRdeZ75szhDiGN8hJs8v8PcqwBm7KsFcp",
"metadata": "customer-123",
"allowedFlows": ["SATOSHI_TEST"],
"satoshiFlow": {
"depositAddress": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080"
}
}'
If satoshiFlow.amount is omitted, CryptoSwift generates a random amount. Share the returned satoshiFlow.depositAddress, asset, and satoshiFlow.amount with the wallet owner.
Then select SATOSHI_TEST and start transaction monitoring:
curl --location 'https://api-dev.cryptoswift.eu/wallet-verification/9f3dc458-a2be-4a34-bcb7-f1f677a0864c/verification' \
--header 'x-api-key: $API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"verificationFlow": "SATOSHI_TEST"
}'
This submission does not create or configure the Satoshi Test; those details come from the existing wallet verification. It starts monitoring for the expected transaction. The verification stays PENDING until CryptoSwift detects a match, then changes to VERIFIED. Satoshi Test verification expires 48 hours after createdAt.
Visual Proof
Visual Proof lets a user upload visual proof of wallet ownership for manual review.
Send the media files as multipart files fields in the same proof-submission request:
POST {{apiBaseUrl}}/wallet-verification/{id}/verification
CryptoSwift receives the file contents from this request and stores them with the wallet verification. The verification then changes to ACTION_REQUIRED so your team can review the evidence.
Mandatory fields:
verificationFlow:VISUAL_PROOFfiles: One to three files, using a separatefilespart for each file
File limits:
- Images and PDFs:
jpeg,jpg,png, orpdf, max 5MB each - Videos:
mp4,webm,mov,avi,wmv,mkv,mpeg,m4v, or3gp, max 50MB each
Test a Local File with curl
The curl example is intended for manual testing. The @ prefix tells curl to read and upload a file that already exists on the machine running the command. Replace the path with the absolute path to your test file.
API_KEY='<api-key>' VERIFICATION_ID='<verification-id>' curl --location --request POST \ 'https://api-dev.cryptoswift.eu/wallet-verification/'"$VERIFICATION_ID"'/verification' \ --header "X-Api-Key: $API_KEY" \ --form 'verificationFlow=VISUAL_PROOF' \ --form 'files=@/absolute/path/to/wallet-screenshot.png'
For multiple files, repeat the files field:
--form 'files=@/absolute/path/to/wallet-screenshot.png' \ --form 'files=@/absolute/path/to/second-proof.pdf'
Upload a Customer-Selected File
In a production integration, the file usually comes from a file picker in your application. The browser sends the selected File objects to your backend. Your backend then forwards the multipart request to CryptoSwift with the API key.
Customer selects files in your UI
↓
Browser sends multipart data to your backend
↓
Your backend streams the multipart body to CryptoSwift
↓
CryptoSwift stores the files and returns the updated verification
Visual proof sets status to ACTION_REQUIRED. Review the uploaded proof and manually update the verification to VERIFIED or DECLINED.
Self-Declaration
Self-declaration lets the user declare wallet ownership without an on-chain transaction, screenshot, or signature.
curl --location 'https://api-dev.cryptoswift.eu/wallet-verification/9f3dc458-a2be-4a34-bcb7-f1f677a0864c/verification' \
--header 'x-api-key: $API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"verificationFlow": "SELF_DECLARED"
}'
Submitting SELF_DECLARED records the wallet owner's declaration and ends the flow immediately by setting status to VERIFIED. No transaction, signature, file, or later monitoring step is required.
3. Receive Notification via Webhook
When a verification is updated or processed, CryptoSwift sends an HTTP POST to your tenant's configured webhook URL. The JSON body represents the current wallet-verification object.
Relevant request headers:
Content-Type: application/jsonX-Event-Type: wallet-verificationCryptoSwift-Signature: t=<timestamp>,s=<signature>
Always verify the CryptoSwift-Signature header before processing the body.
Example SIGNATURE_PROOF payload:
{
"id": "9f3dc458-a2be-4a34-bcb7-f1f677a0864c",
"address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"asset": "ETH",
"blockchain": "Ethereum",
"flow": "SIGNATURE_PROOF",
"metadata": "customer-123",
"status": "VERIFIED",
"statusReasoning": null,
"signature": "0xba467b233bba2bfc76cab7fc684924bc455ccdf75f91ed6e0129ca0527f5585c5992f0efdf8f53eeacc2204fd5c609aac1a149472643c92313d5cbdd6c64b5a11c",
"message": "CryptoSwift wallet ownership verification | verificationId: 9f3dc458-a2be-4a34-bcb7-f1f677a0864c | nonce: 7f9c2a6d8e41b305 | issuedAt: 2026-07-28T10:15:30.000Z",
"createdAt": "2026-07-28T10:14:52.381Z",
"expiresAt": "2026-07-30T10:14:52.381Z",
"updatedAt": "2026-07-28T10:15:34.127Z",
"verifiedTransactionHash": null,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImMzZDJjZjUzLWIzNDEtNGM5YS1hMzliLTY2NTU3OTVmYmU3MiIsIm9yaWdpbiI6Imh0dHBzOi8vYXBwLmV4YW1wbGUuY29tIiwiaWF0IjoxNzg1MjMzNjkyLCJleHAiOjE3ODU0MDY0OTJ9.yli42pzTZoNSB8E5QIrvTA8dHOA7TQnNwJWnXwumqh0",
"origin": "https://app.example.com",
"allowedFlows": [
"SIGNATURE_PROOF",
"VISUAL_PROOF",
"SELF_DECLARED"
],
"redirectUrl": null,
"url": "https://wallet.cryptoswift.eu/?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImMzZDJjZjUzLWIzNDEtNGM5YS1hMzliLTY2NTU3OTVmYmU3MiIsIm9yaWdpbiI6Imh0dHBzOi8vYXBwLmV4YW1wbGUuY29tIiwiaWF0IjoxNzg1MjMzNjkyLCJleHAiOjE3ODU0MDY0OTJ9.yli42pzTZoNSB8E5QIrvTA8dHOA7TQnNwJWnXwumqh0",
"riskScore": 12,
"warnings": [],
"riskSeverity": "low",
"satoshiFlow": {
"amount": null,
"depositAddress": null
},
"reuse": null
}
Fields such as message, signature, verifiedTransactionHash, satoshiFlow, risk results, and reuse details depend on the selected flow and available analysis. Optional fields can be null or omitted.
Process deliveries idempotently: do not assume that each update arrives only once or in order. Treat the webhook as a notification and retrieve the latest verification by id before making compliance decisions.
curl --location 'https://api-dev.cryptoswift.eu/wallet-verification/9f3dc458-a2be-4a34-bcb7-f1f677a0864c' \ --header 'x-api-key: $API_KEY'
4. Manual Updates
For manual review flows, update the verification after your compliance team makes a decision.
curl --location --request PATCH 'https://api-dev.cryptoswift.eu/wallet-verification/9f3dc458-a2be-4a34-bcb7-f1f677a0864c' \
--header 'x-api-key: $API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"status": "VERIFIED",
"statusReasoning": "Manual approval",
"metadata": "case-456"
}'
Supported statuses:
PENDING: Verification has been created and is waiting for proof or completionVERIFIED: Ownership is verifiedACTION_REQUIRED: Manual VASP action is requiredDECLINED: Manual review rejected the proofFAILED: Automated verification failedDELETED: Verification was deleted