Find what a customer can open
Use capabilities to choose an account option for a customer, and requirements to learn exactly what the create request must carry.
Before you open an account, read capabilities to choose an account option for the customer. Then read requirements to learn exactly what the create request must carry.
API version: v2.
Who it is for. Any developer who is about to open a customer's first account and needs to know what the create request must carry.
Status. Available.
Money example
No money moves yet. Ada, a UK resident, wants a US client to pay her 1,200.00 USD.
- You ask capabilities what a GB individual can open in USD, and get several wallet-backed options.
- You ask requirements about the default option, and learn that it needs 13 fields and 2 documents.
- You build one create request with those fields and documents. Ada's account opens with bank details that her client can pay 1,200.00 USD into.
Before you start
- The customer exists, and
kyc[type=basic].statusiscompletedonGET /v1/customers/{customer_id}. For a business, also save eachassociated_persons[].id. - A scoped key needs
account:createfor the create that follows. - Ask the customer these questions:
- Are they an individual or a business?
- Which currency do they want?
- Should deposits arrive as USDC in a wallet, or stay in the currency?
- What is their citizenship country, or their incorporation country?
Find out what is required
1. Ask for the options
Always send customer_type, even with customer_id. Without it, the list mixes both types and the same bank_code can appear twice with different limits.
curl --request GET \
--url 'https://api.gravv.xyz/v1/accounts/capabilities?customer_id=0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33&customer_type=individual¤cy=USD' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/accounts/capabilities?customer_id=0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33&customer_type=individual¤cy=USD", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os
import requests
response = requests.get(
"https://api.gravv.xyz/v1/accounts/capabilities?customer_id=0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33&customer_type=individual¤cy=USD",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
"data": {
"country": "GB",
"customer_type": "individual",
"subdivision": "GB-LND",
"options": [
{
"account_types": ["card", "operating", "revenue", "yield"],
"bank_code": "<bank_code>",
"bank_name": "<bank name>",
"categories": ["main", "sub_account"],
"currency": "USD",
"excluded_countries": ["AF", "IN", "KE"],
"excluded_subdivisions": [],
"features": {"collections": true, "internal_transfers": true},
"max_accounts_per_customer": 20,
"rails": [
{"cost": "low", "currency": "USD", "description": "Domestic US bank transfers over the Automated Clearing House network.", "rail": "ach", "settlement_time": "1–3 business days", "title": "ACH"}
],
"requires_customer_record": true,
"token_wallets": [{"asset": "usdc", "network": "polygon"}, {"asset": "usdc", "network": "ethereum"}, {"asset": "usdc", "network": "solana"}]
}
]
},
"error": null
}
Pick one option and note these fields:
bank_code. Copy it into the next calls. Do not hard-code it.token_wallets. If it is not empty, the option is wallet-backed, so sendnetwork. If it is empty, the option is fiat-only, so send nonetwork.max_accounts_per_customer.nullmeans no limit. Withcustomer_id, the value includes any limit set for this customer.rails. These are the ways payers can send money in.[]means Gravv publishes none, so ask support before you promise a rail.
Gravv already filters the list by the customer's country. Without a customer, send country instead of customer_id. For the US, also send subdivision, such as US-NY.
2. Read the requirements for that option
Always send bank_code and country with the customer's customer_id. The create call picks its route by the country you send. Without both, you can read the form of a different bank from the one create picks.
curl --request GET \
--url 'https://api.gravv.xyz/v2/accounts/requirements?customer_type=individual¤cy=USD&bank_code=<bank_code>&country=GB&customer_id=0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v2/accounts/requirements?customer_type=individual¤cy=USD&bank_code=<bank_code>&country=GB&customer_id=0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os
import requests
response = requests.get(
"https://api.gravv.xyz/v2/accounts/requirements?customer_type=individual¤cy=USD&bank_code=<bank_code>&country=GB&customer_id=0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
"data": {
"documents": [
{"new_document_fields": ["document_type", "content", "issuing_country", "number", "issued_date", "expiration_date"], "required": true, "scope": "root", "types": ["national_id", "passport", "drivers_license", "permanent_resident_card", "employment_authorization", "other"]},
{"new_document_fields": ["document_type", "content", "issuing_country"], "required": true, "scope": "root", "types": ["proof_of_address"]}
],
"fields": [
{"group": "identity", "is_predefined": true, "label": "First Name", "name": "first_name", "required": true, "scope": "root", "type": "string", "write_only": false},
{"enum": "Gender", "group": "identity", "is_predefined": true, "label": "Gender", "name": "gender", "options": ["female", "male"], "required": true, "scope": "root", "type": "enum", "write_only": false},
{"applies_when": {"field": "citizenship_country", "op": "eq", "value": "US"}, "group": "us_tax", "is_predefined": false, "label": "Tax ID Number", "name": "tax_id_number", "required": true, "scope": "root", "type": "string", "write_only": true}
],
"has_existing_ref": false,
"max_parties": null,
"min_parties": null,
"suggestions": {
"documents": {"passport": {"message": "already on file for this customer; upload a new one to override it", "value": "passport"}},
"fields": {"first_name": {"message": "on file for this customer; send this field to override it", "value": "Ada"}}
},
"token_options": [{"asset": "usdc", "network": "polygon"}, {"asset": "usdc", "network": "ethereum"}, {"asset": "usdc", "network": "solana"}]
},
"error": null
}
3. Turn the form into a request
| Requirements says | Put it in the create body |
|---|---|
fields[] with scope root |
data.root.<name>. A dotted name nests, so permanent_address.city becomes "permanent_address": {"city": "…"} |
fields[] with scope party[*] |
Every data.parties[] entry (a business's directors, owners and signers) |
new_party_fields[] |
A party you declare without id, for a person not on the customer record |
documents[] with scope root |
data.documents[], with one document of any one of the types |
documents[] with scope party[*] |
That party's own documents[] |
new_document_fields |
The keys a new document carries. content is raw base64 of a JPEG, PNG or PDF, at most 2 MB. In sandbox, keep all of one request's files under 500 KB together |
required: true and no condition |
Always send it |
applies_when |
Send it only when the condition holds for this customer, such as only for US citizens |
options[] |
The only accepted values |
write_only: true |
A secret, such as a tax number. The API never returns it |
is_predefined: true, or a key in suggestions.fields |
The value is on the customer record already. Leave it out and Gravv fills it, or send it to override |
suggestions.documents.<type> |
A document of that type is on file |
token_options[] |
The networks you may send as network |
min_parties, max_parties |
How many parties a business declares |
has_existing_ref: true |
The bank already knows this customer. Send no data.root (Open a second account) |
existing_application |
An application is open at this bank. Fix that one |
The form does not show these rules:
- These fields go at the top level, not in
data.root:customer_id,customer_type,currency,account_type,category,label,bank_code,network,wallet_id. - Send
citizenship_country(a person) orincorporation_country(a business) indata.rootyourself. Prefill takes these from the customer's address country, which can be different. - Prefill covers names,
date_of_birth,email, phones,gender,legal_name,registration_number,business_description,business_typeandindustry. Prefill does not cover the address, so send it. - If you send no documents, Gravv attaches the customer's documents on file. If you send even one, Gravv uses only the documents you send. To reuse one file, send
{"id": "<document id>"}instead ofcontent. categorydefaults tomain, which means your own business's account. Sendsub_accountfor a customer's account.
Gravv supports stellar, polygon, ethereum, avalanche_c_chain and solana for wallet-backed accounts. Pick one of these that the option lists. Treat any other network an option lists as Not supported. If in doubt, use polygon, which every wallet-backed option offers.
Steps
- Confirm
kyc[type=basic].statusiscompleted. - Read capabilities and pick the option. Note its
bank_code, its backing andmax_accounts_per_customer. - Read requirements for that
bank_codeand builddata. - Go to the recipe for your currency and backing in Accounts (v2) to send
POST /v2/accounts.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 a customer country is required: supply country (ISO-3166 alpha-2) or a customer_id whose profile has an address |
You sent no country, and the customer has no address |
Send country |
400 invalid request payload: unknown field "limit" |
These calls refuse unknown query parameters | Send only the documented ones |
400 Customer Type is required (and 1 other field; see details) |
customer_type and currency are required on requirements |
Send both |
400 no account option is available for the requested combination (customer_type=individual, currency=USD, country=GB); … |
No option serves the values. The API also returns this for an unknown bank_code, or for one that does not serve that country or currency. The text never names bank_code |
Copy bank_code, currency and country from one listed option |
| The business form comes back for an individual customer | Requirements does not check the type against the customer | Send the customer's own type. Create refuses a mismatch |
No suggestions even with customer_id |
The customer lookup failed | Retry. The form itself is still right |
| 504 on capabilities | The API did not answer within 10 seconds | Retry |
Webhooks you will receive
These reads send no events. Before them, you receive customer.created and customer.kyc.status.*.
Testing in sandbox
Both calls work in sandbox. Sandbox can list different options, limits and excluded countries from live. Its token_options can be shorter than token_wallets, so send a network from token_options.