Upload Document
curl --request POST \
--url https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/documents \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'X-API-Key: <api-key>' \
--data '
{
"requirement_key": "<string>",
"type": "<string>",
"file": "<string>",
"side": "<string>",
"file_name": "<string>",
"country_code": "<string>"
}
'import requests
url = "https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/documents"
payload = {
"requirement_key": "<string>",
"type": "<string>",
"file": "<string>",
"side": "<string>",
"file_name": "<string>",
"country_code": "<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({
requirement_key: '<string>',
type: '<string>',
file: '<string>',
side: '<string>',
file_name: '<string>',
country_code: '<string>'
})
};
fetch('https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/documents', 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}/documents",
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([
'requirement_key' => '<string>',
'type' => '<string>',
'file' => '<string>',
'side' => '<string>',
'file_name' => '<string>',
'country_code' => '<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}/documents"
payload := strings.NewReader("{\n \"requirement_key\": \"<string>\",\n \"type\": \"<string>\",\n \"file\": \"<string>\",\n \"side\": \"<string>\",\n \"file_name\": \"<string>\",\n \"country_code\": \"<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}/documents")
.header("Idempotency-Key", "<idempotency-key>")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"requirement_key\": \"<string>\",\n \"type\": \"<string>\",\n \"file\": \"<string>\",\n \"side\": \"<string>\",\n \"file_name\": \"<string>\",\n \"country_code\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/documents")
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 \"requirement_key\": \"<string>\",\n \"type\": \"<string>\",\n \"file\": \"<string>\",\n \"side\": \"<string>\",\n \"file_name\": \"<string>\",\n \"country_code\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"object": "<string>",
"account_id": "<string>",
"virtual_account_group_id": "<string>",
"requirement_key": "<string>",
"type": "<string>",
"side": "<string>",
"file_name": "<string>",
"content_type": "<string>",
"size_bytes": 123,
"submission_status": "<string>",
"country_code": "<string>",
"created": "<string>",
"updated": "<string>",
"created_by": "<string>",
"updated_by": "<string>"
}Virtual Account Groups
Upload Document
Upload evidence that satisfies one outstanding document requirement on a virtual account group
POST
/
v1
/
accounts
/
{account_id}
/
virtual-account-groups
/
{group_id}
/
documents
Upload Document
curl --request POST \
--url https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/documents \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'X-API-Key: <api-key>' \
--data '
{
"requirement_key": "<string>",
"type": "<string>",
"file": "<string>",
"side": "<string>",
"file_name": "<string>",
"country_code": "<string>"
}
'import requests
url = "https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/documents"
payload = {
"requirement_key": "<string>",
"type": "<string>",
"file": "<string>",
"side": "<string>",
"file_name": "<string>",
"country_code": "<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({
requirement_key: '<string>',
type: '<string>',
file: '<string>',
side: '<string>',
file_name: '<string>',
country_code: '<string>'
})
};
fetch('https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/documents', 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}/documents",
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([
'requirement_key' => '<string>',
'type' => '<string>',
'file' => '<string>',
'side' => '<string>',
'file_name' => '<string>',
'country_code' => '<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}/documents"
payload := strings.NewReader("{\n \"requirement_key\": \"<string>\",\n \"type\": \"<string>\",\n \"file\": \"<string>\",\n \"side\": \"<string>\",\n \"file_name\": \"<string>\",\n \"country_code\": \"<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}/documents")
.header("Idempotency-Key", "<idempotency-key>")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"requirement_key\": \"<string>\",\n \"type\": \"<string>\",\n \"file\": \"<string>\",\n \"side\": \"<string>\",\n \"file_name\": \"<string>\",\n \"country_code\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hopnow.io/v1/accounts/{account_id}/virtual-account-groups/{group_id}/documents")
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 \"requirement_key\": \"<string>\",\n \"type\": \"<string>\",\n \"file\": \"<string>\",\n \"side\": \"<string>\",\n \"file_name\": \"<string>\",\n \"country_code\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"object": "<string>",
"account_id": "<string>",
"virtual_account_group_id": "<string>",
"requirement_key": "<string>",
"type": "<string>",
"side": "<string>",
"file_name": "<string>",
"content_type": "<string>",
"size_bytes": 123,
"submission_status": "<string>",
"country_code": "<string>",
"created": "<string>",
"updated": "<string>",
"created_by": "<string>",
"updated_by": "<string>"
}This endpoint works on any account you own. Create Virtual Account Group covers who can open a group and what the account needs first.
The onboarding opened by Create Virtual Account Group asks for evidence one requirement at a time. Get Virtual Account Group lists those outstanding requirements, each with a
This route accepts a request body of up to 21 MB (22,020,096 bytes), instead of the 1 MB that applies everywhere else on this API. Base64 encoding adds about a third, so the practical ceiling is a file of roughly 15 MB. An oversized body is rejected with
key and the document types it accepts; this endpoint uploads a file against exactly one of them, using requirement_key to say which. A requirement_key that is not currently open on this group is rejected.
The file is carried inside the JSON body as a base64 data URI, so there is no multipart upload. Hop stores it as account evidence, links it to the requirement, and sends it to the provider in the same request. A successful response always reports submission_status as submitted.
Accept every agreement via Submit Agreement Actions and clear every document requirement to enable the group, then create virtual accounts under it.
Requires an API key with write access on the virtual_accounts scope.
Path Parameters
string
required
The account’s external ID (starts with
acct_)string
required
The virtual account group’s external ID (starts with
vag_). It must belong to the account in the path.Headers
string
required
Canonical lowercase 8-4-4-4-12 UUID. Scoped to the group, with the payload fingerprinted including the file’s bytes: replaying the same key with the same payload returns the stored document with
200, and with a different payload returns 409. See Idempotency.Request Body
Unknown fields are rejected.string
required
The
key of the open document requirement this evidence satisfies, copied from Get Virtual Account Group (1-200 characters). A Hop-normalised key, never a provider code.string
required
Document type for this upload. Do not treat the document types below as a checklist. Required documents are returned in
requirements by the Get Virtual Account Group endpoint. For each requirement where type is document, copy its key to requirement_key and choose one of its accepted_document_types. Types listed for the same requirement are alternatives or substitutes, not all are required. If multiple document requirements are returned, each must be satisfied separately. The list below shows all document types supported by the endpoint.Identity: passport, national_id, drivers_license, residence_permit, selfie.Company: certificate_of_incorporation, articles_of_association, memorandum_of_association, constitution, by_laws, operating_agreement, regulatory_license, certificate_of_good_standing, register_of_directors, register_of_shareholders, organization_chart, corporate_structure.Other evidence: proof_of_address, bank_statement, board_resolution, source_of_funds, source_of_wealth, aml_policy, proof_of_directorship, other.string
required
The file as a base64 data URI:
data:<content-type>;base64,<base64 payload>, with a valid, non-empty base64 payload. The content type inside the URI must be application/pdf, image/jpeg or image/png.string
front or back. Required for a two-sided identity document, national_id, drivers_license and residence_permit, and rejected for every other type. Upload each side as its own request.string
Original file name to store with the evidence (1-255 characters), or omit it
string
Country the evidence was issued in, as an ISO 3166-1 alpha-2 code, for example
GB413 REQUEST_TOO_LARGE and details.max_bytes carries the limit.
Response
Returns201 with the stored document on first upload, or 200 with the same document when the Idempotency-Key was already used with the same payload.
string
Document identifier (starts with
doc_). Evidence is stored at account level, so the same document id can appear under more than one group.string
Always returns
"virtual_account_group_document"string
Account the evidence belongs to (starts with
acct_)string
Virtual account group the evidence was linked to (starts with
vag_)string
The requirement this evidence satisfies, echoing the request
string
Document type, echoing the request
string
front or back for a two-sided identity document, otherwise nullstring
Stored file name, or
null when none was sentstring
Media type taken from the data URI:
application/pdf, image/jpeg or image/pnginteger
Size of the decoded file in bytes
string
Delivery state towards the provider. Always
submitted on a successful response, since delivery happens before the response is returned. The other values, pending, failed and unknown, are visible in List Documents.string
ISO 3166-1 alpha-2 country of issue, or
nullstring
ISO 8601 timestamp when created
string
ISO 8601 timestamp when last updated
string
ID of the API key that uploaded the document (starts with
ak_)string
Actor that last updated the document, or
nullWhen delivery to the provider fails
The file is stored and linked before it is sent on. If the provider then rejects the delivery, the request fails with502 and no document is returned, but the evidence is kept and its link is marked failed. Retry with the same Idempotency-Key and the same payload: Hop finds the stored evidence and re-attempts delivery instead of creating a second document. A fresh key on the same file uploads it again.
Errors
| Status | code | When |
|---|---|---|
404 | RESOURCE_NOT_FOUND | group_id is unknown or belongs to another account |
409 | BUSINESS_RULE_VIOLATION | The Idempotency-Key was already used on this group with a different document payload |
413 | REQUEST_TOO_LARGE | The body is over 21 MB; details.max_bytes carries the limit |
422 | VALIDATION_ERROR | Idempotency-Key is missing or not a canonical 8-4-4-4-12 UUID |
422 | VALIDATION_ERROR | side is missing on a two-sided identity document, or sent on a type that does not take one |
422 | VALIDATION_ERROR | The body carries an unknown field |
422 | VALIDATION_ERROR | The data URI’s content type is not application/pdf, image/jpeg or image/png; details.content_type carries it |
422 | DOCUMENT_SUBJECT_MISMATCH | The document type does not fit its subject: a company document on an individual account or with a person attached, or an identity document on a business account with no person to attach it to. details carry document_type and required_subject (account or person) |
422 | INVALID_FIELD_FORMAT | file is not a base64 data URI, or its payload is empty or not valid base64 |
422 | INVALID_FIELD_VALUE | requirement_key is not an open document requirement on this group: details carry requirement_key |
422 | INVALID_FIELD_VALUE | type is not one the requirement accepts: details carry requirement_key, document_type and accepted_document_types |
502 | GATEWAY_ERROR | The provider rejected the delivery. The evidence is stored; retry with the same key. |
Request Example
curl -X POST "https://api.hopnow.io/v1/accounts/acct_ka44qsvpo8q3wtzuwfqf0h6u/virtual-account-groups/vag_7m2k9x4c1b6v3n8q5z0j2p7t/documents" \
-H "X-API-Key: your_api_key" \
-H "X-Signature: hmac_signature" \
-H "X-Timestamp: 1234567890" \
-H "X-Nonce: abc123" \
-H "Idempotency-Key: 3f8c1b24-6d5e-4a7f-9c10-2b4e8d6a1f37" \
-H "Content-Type: application/json" \
-d '{
"requirement_key": "req_9c1f4a7b2e06d38a5f4c1b72",
"type": "passport",
"file_name": "passport.pdf",
"country_code": "GB",
"file": "data:application/pdf;base64,JVBERi0xLjcKJcTl8uXrp..."
}'
Response Example
{
"id": "doc_xw9rlpdt0bcr5u4f9n095lf4",
"object": "virtual_account_group_document",
"account_id": "acct_ka44qsvpo8q3wtzuwfqf0h6u",
"virtual_account_group_id": "vag_7m2k9x4c1b6v3n8q5z0j2p7t",
"requirement_key": "req_9c1f4a7b2e06d38a5f4c1b72",
"type": "passport",
"side": null,
"file_name": "passport.pdf",
"content_type": "application/pdf",
"size_bytes": 248312,
"submission_status": "submitted",
"country_code": "GB",
"created": "2026-01-16T09:20:00Z",
"updated": "2026-01-16T09:20:00Z",
"created_by": "ak_3f7k9m2p5r8t1v4x6z0b3n5q",
"updated_by": null
}