Request a virtual account for a user
curl --request POST \
--url https://api.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding \
--header 'Content-Type: application/json' \
--header 'partner-api-key: <api-key>' \
--data '
{
"country": "US"
}
'import requests
url = "https://api.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding"
payload = { "country": "US" }
headers = {
"partner-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'partner-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({country: 'US'})
};
fetch('https://api.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding', 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.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding",
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([
'country' => 'US'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"partner-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.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding"
payload := strings.NewReader("{\n \"country\": \"US\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("partner-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.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding")
.header("partner-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"country\": \"US\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["partner-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"country\": \"US\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"status": "SUBMITTED",
"onboarding": {
"id": "0f1f0d0a-9c62-4d0e-9a3f-0b0f2f8a1c34",
"country": "US",
"status": "IN_PROGRESS",
"createdAt": "2026-08-25T14:03:11.482Z",
"updatedAt": "2026-08-25T14:03:11.482Z"
}
}
}
Virtual Accounts
Request Virtual Account (User)
Starts banking onboarding for one of your users so Capa can provision a named virtual account in their own name. Onboarding runs asynchronously — a SUBMITTED response only means the request was accepted. Poll the status endpoint until it reports COMPLETED, then read the provisioned account from the bank accounts endpoint. Currently available for the United States only.
POST
/
api
/
partner
/
v2
/
banks
/
users
/
{userId}
/
banking-onboarding
Request a virtual account for a user
curl --request POST \
--url https://api.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding \
--header 'Content-Type: application/json' \
--header 'partner-api-key: <api-key>' \
--data '
{
"country": "US"
}
'import requests
url = "https://api.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding"
payload = { "country": "US" }
headers = {
"partner-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'partner-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({country: 'US'})
};
fetch('https://api.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding', 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.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding",
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([
'country' => 'US'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"partner-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.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding"
payload := strings.NewReader("{\n \"country\": \"US\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("partner-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.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding")
.header("partner-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"country\": \"US\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.sandbox.capa.fi/api/partner/v2/banks/users/{userId}/banking-onboarding")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["partner-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"country\": \"US\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"status": "SUBMITTED",
"onboarding": {
"id": "0f1f0d0a-9c62-4d0e-9a3f-0b0f2f8a1c34",
"country": "US",
"status": "IN_PROGRESS",
"createdAt": "2026-08-25T14:03:11.482Z",
"updatedAt": "2026-08-25T14:03:11.482Z"
}
}
}
Starts banking onboarding for one of your users so Capa can provision a named virtual account — a US bank account (account number + ABA routing number) issued in the user’s own name. See the Virtual Accounts guide for the full flow.
Currently available for the United States (
The endpoint is safe to call repeatedly: when an onboarding is already in flight or already complete, it returns the existing record instead of starting a second one.
Once KYB is complete, call this endpoint again.
US) only. Any other country value is rejected with 400.
Onboarding is asynchronous. A
SUBMITTED response only means the request was accepted — the account is not usable yet. Poll Get Virtual Account Status until it reports COMPLETED, then read the account from List Bank Accounts.Response Statuses
A201 does not always mean a new onboarding was started. Always branch on data.status:
status | Meaning | What to do |
|---|---|---|
SUBMITTED | Onboarding was created and sent to the banking provider. | Poll the status endpoint until COMPLETED. |
ALREADY_IN_PROGRESS | An onboarding for this user and country is already running. Nothing new was created. | Keep polling — the call is safe to repeat. |
ALREADY_COMPLETED | The user already completed onboarding for this country. | Read the account from List Bank Accounts. |
REQUIREMENTS_MISSING | The user’s KYB is not complete. Nothing was submitted. | Send the user to kybLink to finish KYB, then retry. |
Missing Requirements
status: "REQUIREMENTS_MISSING" means the user’s KYB is not complete. Nothing was submitted. The response carries:
kybLink— a hosted link where the user can finish their KYB. It may be absent if a link could not be minted; in that case, direct the user through your usual KYB flow.missingRequirements— what is still outstanding, each entry with afieldpath and a human-readablemessage. Useful for surfacing a reason in your UI, but you do not need to collect these fields yourself — completing KYB satisfies them.
{
"success": true,
"data": {
"status": "REQUIREMENTS_MISSING",
"kybLink": "https://verify.capa.fi/s/2f9c1b7e",
"missingRequirements": [
{ "field": "taxId", "message": "Tax ID is required for business onboarding" }
]
}
}
Error Codes
Common Errors
| HTTP Status | Code | Message |
|---|---|---|
| 401 | UNAUTHORIZED | ”API Key is missing” |
| 401 | UNAUTHORIZED | ”Invalid API Key format” |
| 401 | UNAUTHORIZED | ”Invalid API Key” |
| 403 | INVALID_PARTNER_FLOW | ”The partner has an invalid flow.” |
User Ownership Errors
| HTTP Status | Code | Message |
|---|---|---|
| 401 | UNAUTHORIZED | ”Partner information is required for this operation” |
| 401 | UNAUTHORIZED | ”User is not associated with the partner” |
Endpoint-Specific Errors
| HTTP Status | Code | Message |
|---|---|---|
| 400 | INVALID_USER_INPUT_ERROR | ”Invalid User Input” — country is missing, not US, or userId is not a valid UUID |
| 404 | NOT_FOUND_ERROR | ”User not found” |
Authorizations
API key for the affiliated partner performing the request.
Path Parameters
The ID of the user to onboard. Must belong to the authenticated partner.
Example:
"8374f327-38bd-4b0b-b8a7-2524599eb903"
Body
application/json
The country whose banking provider should issue the virtual account. Only US is currently supported.
Available options:
US Example:
"US"