Create Virtual Account
curl --request POST \
--url https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'X-API-Key: <api-key>' \
--data '
{
"currency": "<string>",
"label": "<string>"
}
'import requests
url = "https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts"
payload = {
"currency": "<string>",
"label": "<string>"
}
headers = {
"Idempotency-Key": "<idempotency-key>",
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'Idempotency-Key': '<idempotency-key>',
'X-API-Key': '<api-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({currency: '<string>', label: '<string>'})
};
fetch('https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts', 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.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts",
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([
'currency' => '<string>',
'label' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Idempotency-Key: <idempotency-key>",
"X-API-Key: <api-key>"
],
]);
$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.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts"
payload := strings.NewReader("{\n \"currency\": \"<string>\",\n \"label\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("X-API-Key", "<api-key>")
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.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts")
.header("Idempotency-Key", "<idempotency-key>")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"currency\": \"<string>\",\n \"label\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Idempotency-Key"] = '<idempotency-key>'
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"currency\": \"<string>\",\n \"label\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"object": "<string>",
"account_id": "<string>",
"currency": "<string>",
"status": "<string>",
"label": "<string>",
"balances": {
"available": "<string>",
"pending_payin": "<string>",
"held_for_payout": "<string>",
"total": "<string>"
},
"beneficiary_name": "<string>",
"bank_name": "<string>",
"account_number": "<string>",
"account_number_last4": "<string>",
"iban": "<string>",
"swift_bic": "<string>",
"routing_number": "<string>",
"routing_number_type": "<string>",
"bank_address": {},
"beneficiary_address": {},
"provisioned_at": "<string>",
"created": "<string>",
"updated": "<string>",
"created_by": "<string>",
"updated_by": "<string>"
}Virtual Accounts
Create Virtual Account
Provision a named virtual account in one currency inside a virtual account group
POST
/
v1
/
accounts
/
{account_id}
/
virtual-account-groups
/
{group_id}
/
virtual-accounts
Create Virtual Account
curl --request POST \
--url https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'X-API-Key: <api-key>' \
--data '
{
"currency": "<string>",
"label": "<string>"
}
'import requests
url = "https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts"
payload = {
"currency": "<string>",
"label": "<string>"
}
headers = {
"Idempotency-Key": "<idempotency-key>",
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'Idempotency-Key': '<idempotency-key>',
'X-API-Key': '<api-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({currency: '<string>', label: '<string>'})
};
fetch('https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts', 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.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts",
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([
'currency' => '<string>',
'label' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Idempotency-Key: <idempotency-key>",
"X-API-Key: <api-key>"
],
]);
$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.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts"
payload := strings.NewReader("{\n \"currency\": \"<string>\",\n \"label\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("X-API-Key", "<api-key>")
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.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts")
.header("Idempotency-Key", "<idempotency-key>")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"currency\": \"<string>\",\n \"label\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/virtual-accounts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Idempotency-Key"] = '<idempotency-key>'
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"currency\": \"<string>\",\n \"label\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"object": "<string>",
"account_id": "<string>",
"currency": "<string>",
"status": "<string>",
"label": "<string>",
"balances": {
"available": "<string>",
"pending_payin": "<string>",
"held_for_payout": "<string>",
"total": "<string>"
},
"beneficiary_name": "<string>",
"bank_name": "<string>",
"account_number": "<string>",
"account_number_last4": "<string>",
"iban": "<string>",
"swift_bic": "<string>",
"routing_number": "<string>",
"routing_number_type": "<string>",
"bank_address": {},
"beneficiary_address": {},
"provisioned_at": "<string>",
"created": "<string>",
"updated": "<string>",
"created_by": "<string>",
"updated_by": "<string>"
}A virtual account is a named bank account in a single currency, opened inside an approved virtual account group. It carries its own bank details for receiving deposits and its own balance, which can fund a payout or be a leg of a transfer.
The group must be through onboarding first:
Replaying a key that already created a virtual account returns that virtual account with
A group may hold several virtual accounts in the same currency. Each call with a new
Once the virtual account is
status enabled and onboarding_status approved. Create Virtual Account Group covers who can open a group and what the account needs to get there: an identity profile, plus an associated person marked as a signer on a business account.
Provisioning is asynchronous. The call returns as soon as Hop has recorded the virtual account and asked the provider to open the underlying bank account: the bank details arrive later.
Requires an API key with write access on the virtual_accounts scope.
A newly created virtual account comes back with status: "pending", provisioned_at: null, and every bank detail field set to null: beneficiary_name, bank_name, account_number, account_number_last4, iban, swift_bic, routing_number, routing_number_type, bank_address and beneficiary_address. It cannot be paid into yet, so do not publish this response as funding instructions.
Poll Get Virtual Account until status is active; at that point provisioned_at is set and the bank details are filled in. Activation depends on the provider opening the account and is not instant. A virtual account can also stay pending indefinitely if the provider never returns payable bank details; contact Hop if one does not activate. Once active it also appears in List Deposit Instructions, which is the surface to hand to a payer.
Path Parameters
string
required
The account’s external ID (starts with
acct_)string
required
The virtual account group’s external ID (starts with
vag_); see Create Virtual Account Group. A group that does not exist on this account returns 404.Headers
string
required
Key for this virtual account, scoped to the account. Canonical lowercase 8-4-4-4-12 UUID, for example
9f8c3a21-4d5e-4b67-8a90-1c2d3e4f5a6b. See Idempotency.200, unchanged, and does not open a second one at the provider or re-charge any creation fee.
Only group_id and currency are compared on a replay. Reusing a key with a different currency, or against a different group, returns 409 IDEMPOTENCY_CONFLICT with details.virtual_account_id naming the virtual account the key already created. Reusing it with a different label is not an error: the original virtual account is returned and the new label is ignored, so a label is never changed this way. Use a fresh key to open a differently labelled virtual account.
While a request with the same key is still in flight, a second one fails fast with 409 IDEMPOTENCY_REQUEST_IN_PROGRESS. Retry after a short backoff.
Request Body
string
required
Currency of the virtual account, for example
EUR. The provider backing the group must support opening an account in it; a currency the group’s provider has no account profile for is rejected with 502.string
Optional label for your own reference (max 255 characters). Not shown to payers, not unique, and cannot be changed through this API.
Idempotency-Key opens another one. There is no per-currency limit on this endpoint, so guard against accidental duplicates by reusing your key on retries.
Response
Returns201 with the new virtual account, or 200 with the existing one when the Idempotency-Key was already used.
string
Virtual account identifier (starts with
va_)string
Always returns
"virtual_account"string
Account ID (starts with
acct_). The response does not repeat group_id.string
Currency code of the virtual account
string
Virtual account status:
pending (created, bank details not yet issued: not payable), active (payable and usable as a payout funding source or transfer leg), frozen (balance movements blocked by Hop) or disabled (closed by Hop; terminal). A create always returns pending.string
Label, or
nullobject
string
Account holder a payer should address the deposit to, or
null while pendingstring
Bank holding the account, or
null while pendingstring
Full account number, or
null while pending and on accounts identified by IBANstring
Masked account number, or the masked IBAN when the account has no account number.
null while pending. Despite the name, this field can carry IBAN digits.string
Full IBAN, or
null while pending and on accounts identified by account numberstring
SWIFT/BIC code, or
nullstring
Routing number or sort code, or
nullstring
What
routing_number holds, for example ROUTING_NUMBER or SORT_CODE; null when there is no routing numberobject
Bank address for wire instructions (
address_line, city, region, postal_code, country), or nullobject
Account holder address for wire instructions, same shape as
bank_address, or nullstring
ISO 8601 timestamp when the bank details became available, or
null while pendingstring
ISO 8601 timestamp when created
string
ISO 8601 timestamp when last updated
string
ID of the API key that created the virtual account (starts with
ak_)string
Actor that last updated the virtual account, or
nullactive, its id can be passed as a payout funding_source, see Create Payout, or as a leg of a same-currency transfer.
Errors
| Status | code | When |
|---|---|---|
404 | RESOURCE_NOT_FOUND | group_id is unknown or belongs to another account |
409 | IDEMPOTENCY_REQUEST_IN_PROGRESS | Another request with this Idempotency-Key is still in flight |
409 | IDEMPOTENCY_CONFLICT | The key was already used for a different currency or a different group; details.virtual_account_id is the existing virtual account |
422 | VALIDATION_ERROR | Idempotency-Key is missing or is not a canonical 8-4-4-4-12 UUID, currency is not a supported code, or label is longer than 255 characters |
422 | VIRTUAL_ACCOUNT_GROUP_NOT_ENABLED | The group’s onboarding is not approved, or the group is not enabled |
422 | INSUFFICIENT_FUNDS | The group charges a creation fee and the paying balance does not cover it |
422 | SERVICE_FEE_BALANCE_MISSING | The group charges a creation fee in a currency the fee-paying account holds no balance in |
502 | GATEWAY_REQUEST_ERROR | The provider rejected the request: no account profile exists for currency, or the group’s provider onboarding reference is not valid |
Request Example
{
"currency": "EUR",
"label": "EU Collections"
}
Response Example
{
"id": "va_6b3n9k1q4t7w2z5m8p0r3d6f",
"object": "virtual_account",
"account_id": "acct_ka44qsvpo8q3wtzuwfqf0h6u",
"currency": "EUR",
"status": "pending",
"label": "EU Collections",
"balances": {
"available": "0.00",
"pending_payin": "0.00",
"held_for_payout": "0.00",
"total": "0.00"
},
"beneficiary_name": null,
"bank_name": null,
"account_number": null,
"account_number_last4": null,
"iban": null,
"swift_bic": null,
"routing_number": null,
"routing_number_type": null,
"bank_address": null,
"beneficiary_address": null,
"provisioned_at": null,
"created": "2026-01-15T10:00:00Z",
"updated": "2026-01-15T10:00:00Z",
"created_by": "ak_3f7k9m2p5r8t1v4x6z0b3n5q",
"updated_by": null
}