Prepare a wallet-ownership proof
curl --request POST \
--url https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"walletAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"blockchain": "ETHEREUM"
}
'import requests
url = "https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare"
payload = {
"walletAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"blockchain": "ETHEREUM"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
walletAddress: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
blockchain: 'ETHEREUM'
})
};
fetch('https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'walletAddress' => '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
'blockchain' => 'ETHEREUM'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare"
payload := strings.NewReader("{\n \"walletAddress\": \"0x71C7656EC7ab88b098defB751B7401B5f6d8976F\",\n \"blockchain\": \"ETHEREUM\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"walletAddress\": \"0x71C7656EC7ab88b098defB751B7401B5f6d8976F\",\n \"blockchain\": \"ETHEREUM\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"walletAddress\": \"0x71C7656EC7ab88b098defB751B7401B5f6d8976F\",\n \"blockchain\": \"ETHEREUM\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"result": {
"walletAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"blockchain": "ETHEREUM",
"message": "I confirm ownership of 0x71C7656EC7ab88b098defB751B7401B5f6d8976F on 2026-07-09 [tok:9f2c41ab...]",
"signedOnUtc": "2026-07-09"
}
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}{
"success": false,
"errors": [
{
"code": "unauthenticated",
"message": "Please authenticate to perform this action."
}
]
}{
"success": false,
"errors": [
{
"code": "forbidden",
"message": "You do not have permission to access this resource."
}
]
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}Virtual Bank Accounts
Prepare a wallet-ownership proof
Get the exact message a self-custody customer must sign to prove they control the destination wallet.
POST
/
accounts
/
{accountId}
/
virtual-bank-accounts
/
prepare
Prepare a wallet-ownership proof
curl --request POST \
--url https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"walletAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"blockchain": "ETHEREUM"
}
'import requests
url = "https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare"
payload = {
"walletAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"blockchain": "ETHEREUM"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
walletAddress: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
blockchain: 'ETHEREUM'
})
};
fetch('https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'walletAddress' => '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
'blockchain' => 'ETHEREUM'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare"
payload := strings.NewReader("{\n \"walletAddress\": \"0x71C7656EC7ab88b098defB751B7401B5f6d8976F\",\n \"blockchain\": \"ETHEREUM\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"walletAddress\": \"0x71C7656EC7ab88b098defB751B7401B5f6d8976F\",\n \"blockchain\": \"ETHEREUM\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.venlyfinance.com/v1/accounts/{accountId}/virtual-bank-accounts/prepare")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"walletAddress\": \"0x71C7656EC7ab88b098defB751B7401B5f6d8976F\",\n \"blockchain\": \"ETHEREUM\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"result": {
"walletAddress": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"blockchain": "ETHEREUM",
"message": "I confirm ownership of 0x71C7656EC7ab88b098defB751B7401B5f6d8976F on 2026-07-09 [tok:9f2c41ab...]",
"signedOnUtc": "2026-07-09"
}
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}{
"success": false,
"errors": [
{
"code": "unauthenticated",
"message": "Please authenticate to perform this action."
}
]
}{
"success": false,
"errors": [
{
"code": "forbidden",
"message": "You do not have permission to access this resource."
}
]
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}{
"success": false,
"errors": [
{
"code": "invalid-request",
"message": "The request contains invalid parameters."
}
],
"result": {}
}Requires scope:
Submit the message and signature in the
manage:accounts — see Required scopes.
Returns the message a self-custody customer must sign to prove they control the destination wallet of a virtual bank account.
Nothing is persisted — there is no preparation id and no expiry, so two calls for the same account and wallet return an equivalent message.
The customer must sign
message verbatim and blind. It embeds an opaque provider token, so do not reformat, re-encode, pretty-print, or reconstruct it — the signature is checked against the exact bytes, and a mismatch fails with signature-mismatch.ownershipProof block of Create a virtual bank account.
Not every account needs this step. Call it to find out: if a proof is required you get the message to sign, and if it isn’t you get 400 ownership-proof-not-applicable and can create the account directly.
Only an externally-owned account can sign — the scheme is EIP-191, and ERC-1271 contract signatures are not supported.
Errors
The codes this endpoint can return, in addition to the standard errors. Branch oncode, never the message.
| HTTP | code | When |
|---|---|---|
400 | validation-error | walletAddress or blockchain is missing |
400 | invalid-request | unsupported blockchain |
400 | ownership-proof-not-applicable | this account does not require a customer-signed wallet-ownership proof |
400 | account-holder-unresolved | the account does not resolve to exactly one active account-holder party |
404 | account-not-found | no such account for your company |
404 | wallet-not-found | walletAddress is not this account’s self-custody wallet on blockchain |
Authorizations
OAuth2 client credentials flow. Token endpoints:
Path Parameters
Unique account identifier
Body
application/json
Request to assemble the wallet-ownership-proof message for a virtual bank account.
Response
The assembled message to sign
Was this page helpful?

