Create Virtual Account Group
curl --request POST \
--url https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"type": "<string>"
}
'import requests
url = "https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups"
payload = { "type": "<string>" }
headers = {
"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: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({type: '<string>'})
};
fetch('https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups', 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",
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([
'type' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"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"
payload := strings.NewReader("{\n \"type\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
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")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"type\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"type\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"object": "<string>",
"account_id": "<string>",
"type": "<string>",
"status": "<string>",
"onboarding_status": "<string>",
"enabled_at": "<string>",
"suspended_at": "<string>",
"closed_at": "<string>",
"created": "<string>",
"updated": "<string>",
"created_by": "<string>",
"updated_by": "<string>"
}Virtual Account Groups
Create Virtual Account Group
Open a virtual account group for an account you own and start its onboarding
POST
/
v1
/
accounts
/
{account_id}
/
virtual-account-groups
Create Virtual Account Group
curl --request POST \
--url https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"type": "<string>"
}
'import requests
url = "https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups"
payload = { "type": "<string>" }
headers = {
"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: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({type: '<string>'})
};
fetch('https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups', 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",
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([
'type' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"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"
payload := strings.NewReader("{\n \"type\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
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")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"type\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"type\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"object": "<string>",
"account_id": "<string>",
"type": "<string>",
"status": "<string>",
"onboarding_status": "<string>",
"enabled_at": "<string>",
"suspended_at": "<string>",
"closed_at": "<string>",
"created": "<string>",
"updated": "<string>",
"created_by": "<string>",
"updated_by": "<string>"
}Any customer can open a virtual account group on an account it owns, provided the API key carries the
Clear it the way you clear any other requirement: set the identity profile with Update Account, then poll. That resumes onboarding on its own. There is no second call to this endpoint, and the group keeps the id you already hold.
Allowed transitions, as Hop enforces them when the provider reports a change:
virtual_accounts scope: on your own account, or, if you are a platform customer, on any of your end-customer accounts and on your own fee-collection account. The account needs an identity profile before onboarding can progress, and a business account also needs an associated person marked as a signer.
Opens a group of the given type for an account you own. The body names a type from List Group Types.
There is no Idempotency-Key header on this endpoint. It is idempotent on the account and the group type instead: posting the same type for the same account twice returns the group that already exists. The first call returns 201, a call that reuses an existing group returns 200. Branch on the status code, or compare the returned id with the one you already hold.
Requires an API key with write access on the virtual_accounts scope, which is separate from accounts.
What the account needs first
Onboarding runs on the account’s identity, so the account needs an identity profile before it can progress. Set it with Update Account. That call also fixes the account’stype, business or individual, which decides what else is required.
Business accounts
A business agreement must be signed by an authorised signer, so the account needs at least one associated person withis_signer: true. Add them with Create Associated Person. That person’s id is then required as signer_id on Submit Agreement Actions.
Individual accounts
Individual accounts have no associated persons at all: an individual account signs as itself.signer_id must be omitted from Submit Agreement Actions; sending one is rejected.
Opening a group before the identity profile is set
You do not have to wait for the identity profile. Opening a group on an account that has none succeeds and returns201 like any other create. The group carries a requirement naming what is missing, which Get Virtual Account Group returns:
| Field | Value |
|---|---|
key | account_profile.required |
type | account_profile |
label | Add the Account identity profile |
reason | Set the Account identity before onboarding can start |
Onboarding starts immediately
Creating a group starts provider onboarding, so the group that comes back is not ready to use. A new group starts withstatus pending and onboarding_status action_needed, and two separate gates have to clear before virtual accounts can be created under it: the provider has to approve onboarding (onboarding_status approved), and Hop has to enable the group (status enabled).
onboarding_status is provider-neutral and provider-authoritative:
onboarding_status | Meaning |
|---|---|
action_needed | Something is outstanding on your side. Read requirements on Get Virtual Account Group and clear them. Every new group starts here. |
under_review | Everything asked for has been submitted and the provider is reviewing it. Nothing to do but poll. |
approved | The provider approved onboarding. The group still needs Hop to enable it before it can be used. |
rejected | The provider refused onboarding. Terminal: the group never leaves this state. |
suspended | An approved group was suspended by the provider; suspended_at is set. Virtual account creation is blocked until it returns to approved. |
closed | Onboarding was closed permanently; closed_at is set. Terminal. |
action_needed→under_review,approved,rejected,closedunder_review→action_needed,approved,rejected,closedapproved→action_needed,suspended,closedsuspended→action_needed,approved,closedrejectedandclosedare terminal
action_needed at any time when the provider asks for something new, so keep handling requirements after go-live.
The onboarding loop
- Create the group with this endpoint and keep the returned
id. - Read Get Virtual Account Group. The detail response carries
requirements: the outstanding actions, and anagreementssummary. The create response does not. - Clear each requirement. A
documentrequirement is satisfied by Upload Document quoting the requirementkey; anaccount_profilerequirement by setting or correcting the account’s identity:account_profile.requiredmeans no identity profile has been set at all, and anassociated_personrequirement by adding or correcting a person on the account. - Accept the pending agreements with Submit Agreement Actions, using the ids in
agreements.pending_ids: List Agreements has their titles and content. - Poll Get Virtual Account Group until
requirementsis empty,agreements.statusiscompleteandonboarding_statusisapproved. There are no webhook events for group onboarding today, so polling is the only signal. - Wait for Hop to enable the group:
statusbecomesenabledandenabled_atis set. Then create virtual accounts with Create Virtual Account.
Path Parameters
string
required
The account’s external ID (starts with
acct_). It must belong to your customer: your own account, one of your end-customer accounts, or your fee-collection account.Request Body
string
required
The group type to open, copied from
type in List Group Types (1-100 characters). A type that is not configured returns 404 VIRTUAL_ACCOUNT_GROUP_NOT_FOUND. No other body field is accepted.Response
Returns201 with the new group, or 200 with the existing group when one is already open for this account and type.
The response is the group summary. The same shape List Virtual Account Groups returns. It carries no requirements; call Get Virtual Account Group for those.
string
Virtual account group identifier (starts with
vag_)string
Always returns
"virtual_account_group"string
Account the group belongs to (starts with
acct_)string
The group type, matching the
type you sentstring
Activation state, controlled by Hop:
pending, enabled or disabled. A new group is pending; Hop moves it to enabled once onboarding is approved. Only an enabled group whose onboarding_status is approved can back virtual accounts.string
KYC/KYB progress:
action_needed, under_review, approved, rejected, suspended or closed. See the table above.string
ISO 8601 timestamp when Hop first enabled the group, or
nullstring
ISO 8601 timestamp when onboarding most recently became
suspended, or nullstring
ISO 8601 timestamp when onboarding was closed, or
nullstring
ISO 8601 timestamp when created
string
ISO 8601 timestamp when last updated
string
Actor that created the group: the API key id (starts with
ak_) when opened through this API, otherwise the actor Hop recordedstring
Actor that last updated the group, or
nullErrors
| Status | code | When |
|---|---|---|
403 | ACCESS_DENIED | The API key has no virtual_accounts scope: the accounts scope no longer covers virtual accounts |
404 | RESOURCE_NOT_FOUND | account_id is unknown or belongs to another customer |
404 | VIRTUAL_ACCOUNT_GROUP_NOT_FOUND | type is not one of the types returned by List Group Types |
422 | VALIDATION_ERROR | type is missing, empty, longer than 100 characters, or the body carries an unknown field |
500 | VIRTUAL_ACCOUNT_GROUP_ASSIGNMENT_CONFLICT | The account already holds a group whose stored routing no longer matches Hop’s configuration for that type. Contact support; retrying will not clear it. |
502 | GATEWAY_ERROR | The provider could not start onboarding. The group row already exists, so repeat the same request to resume it. |
Request Example
{
"type": "business_virtual_accounts"
}
Response Example
{
"id": "vag_7m2k9x4c1b6v3n8q5z0j2p7t",
"object": "virtual_account_group",
"account_id": "acct_ka44qsvpo8q3wtzuwfqf0h6u",
"type": "business_virtual_accounts",
"status": "pending",
"onboarding_status": "action_needed",
"enabled_at": null,
"suspended_at": null,
"closed_at": null,
"created": "2026-01-15T10:00:00Z",
"updated": "2026-01-15T10:00:00Z",
"created_by": "ak_3f7k9m2p5r8t1v4x6z0b3n5q",
"updated_by": null
}