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": {}
}Prepare a wallet-ownership proof
Get the exact message a self-custody customer must sign to prove they control the destination wallet.
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": {}
}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.
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. Self-custody wallets are single-key wallets only for now.
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:
Headers
Which tenant the request is scoped to, among those your token grants. Omit it when the token grants exactly one tenant; it is required when the token grants more than one. A supplied value must exactly match a tenant the token grants.
Absent when required, blank, malformed, or naming a tenant the token does not grant — each gets
the same generic 403 forbidden, which reveals neither whether a tenant exists nor which ones
you may use. Send the header once: a repeated header is rejected the same way.
"3fa85f64-5717-4562-b3fc-2c963f66afa6"
Path Parameters
Unique account identifier
Body
Request to assemble the wallet-ownership-proof message for a virtual bank account.
Response
The assembled message to sign
Was this page helpful?

