Skip to content

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:

  1. Create a wallet verification request.
  2. Submit proof of ownership for the selected flow.
  3. 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"
}'
Always double-check you are using the correct environment when integrating. Using the wrong base URL or API key will result in authentication errors.

Mandatory fields:

  • asset: The virtual asset, for example ETH or BTC
  • blockchain: The blockchain name or supported EVM chain ID
  • address: Self-hosted wallet address to verify

Optional fields:

  • metadata: Your own wallet, user, or case reference
  • allowedFlows: Restrict which proof methods can be used
  • origin: Required when validating browser-origin-bound signature messages
  • satoshiFlow: Required when you allow SATOSHI_TEST; include depositAddress and optional amount
  • reuse: 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 match
  • MEDIUM: Address, blockchain, and name hash match
  • HIGH: 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 flowPrimary request formatWhat submission does
SIGNATURE_PROOFapplication/jsonVerifies the submitted message and signature against the wallet address. A valid proof completes the flow with VERIFIED.
SELF_DECLAREDapplication/jsonRecords the wallet owner's declaration and immediately completes the flow with VERIFIED.
SATOSHI_TESTapplication/jsonStarts monitoring for the configured transaction. The verification remains PENDING until CryptoSwift detects a match.
VISUAL_PROOFmultipart/form-dataUploads 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.

OpenAPI content type

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.

Keep your API key on the server

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_PROOF
  • message: The exact, non-empty plain-text message signed by the wallet
  • signature: 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.

EIP-712 typed data is not supported

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 signMessage or personal_sign; the wallet adds the EIP-191 prefix.

Direct API Versus Widget Flow

IntegrationMessage-content validationCryptographic validation
Direct API submissionYou 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 widgetAdditional 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>"
  }'
Always double-check you are using the correct environment when integrating. Using the wrong base URL or API key will result in authentication errors.

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

ProblemWhat to check
Empty message or signatureBoth message and signature are required. The message must contain at least one character.
Invalid or non-hex EVM signatureSubmit the complete signature returned by the wallet, including its 0x prefix. Use an EIP-191 message-signing method supported by the wallet.
Address mismatchConfirm 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 signingSubmit the exact original string. Check capitalization, whitespace, line breaks, punctuation, and text encoding.
EIP-712 typed-data signatureSign again with personal_sign, viem signMessage, or ethers signMessage. signTypedData and EIP-3009 authorization signatures are not valid ownership proofs.
Incorrect request formatPrefer 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 expiredRetrieve 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_PROOF
  • files: One to three files, using a separate files part for each file

File limits:

  • Images and PDFs: jpeg, jpg, png, or pdf, max 5MB each
  • Videos: mp4, webm, mov, avi, wmv, mkv, mpeg, m4v, or 3gp, 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'
Always double-check you are using the correct environment when integrating. Using the wrong base URL or API key will result in authentication errors.

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/json
  • X-Event-Type: wallet-verification
  • CryptoSwift-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 completion
  • VERIFIED: Ownership is verified
  • ACTION_REQUIRED: Manual VASP action is required
  • DECLINED: Manual review rejected the proof
  • FAILED: Automated verification failed
  • DELETED: Verification was deleted

Next steps