---
title: "Find what a customer can open"
description: "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.

1. You ask capabilities what a GB individual can open in USD, and get several wallet-backed options.
2. You ask requirements about the default option, and learn that it needs 13 fields and 2 documents.
3. 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].status` is `completed` on [`GET /v1/customers/{customer_id}`](/api-reference/customers/get-v1-customers-customer-id). For a business, also save each `associated_persons[].id`.
- A scoped key needs `account:create` for 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.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url 'https://api.gravv.xyz/v1/accounts/capabilities?customer_id=0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33&customer_type=individual&currency=USD' \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/accounts/capabilities?customer_id=0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33&customer_type=individual&currency=USD", {
  method: "GET",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
  },
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.get(
    "https://api.gravv.xyz/v1/accounts/capabilities?customer_id=0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33&customer_type=individual&currency=USD",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response, trimmed to one option"
{
  "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 send `network`. If it is empty, the option is fiat-only, so send no `network`.
- `max_accounts_per_customer`. `null` means no limit. With `customer_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.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url 'https://api.gravv.xyz/v2/accounts/requirements?customer_type=individual&currency=USD&bank_code=<bank_code>&country=GB&customer_id=0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33' \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v2/accounts/requirements?customer_type=individual&currency=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());
```
```python title="Python"
import os

import requests

response = requests.get(
    "https://api.gravv.xyz/v2/accounts/requirements?customer_type=individual&currency=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())
```
</CodeGroup>

```json title="Response, trimmed to 3 fields and 2 suggestions"
{
  "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](/recipes/accounts-v2/open-a-second-account)) |
| `existing_application` | An application is open at this bank. [Fix that one](/recipes/accounts-v2/unblock-an-application) |

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) or `incorporation_country` (a business) in `data.root` yourself. 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_type` and `industry`. 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 of `content`.
- `category` defaults to `main`, which means your own business's account. Send `sub_account` for 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

1. Confirm `kyc[type=basic].status` is `completed`.
2. Read capabilities and pick the option. Note its `bank_code`, its backing and `max_accounts_per_customer`.
3. Read requirements for that `bank_code` and build `data`.
4. Go to the recipe for your currency and backing in [Accounts (v2)](/recipes/accounts-v2/overview#choose-your-recipe) 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`.

## Reference

- [Get account capabilities](/api-reference/accounts-v2/get-v1-accounts-capabilities)
- [Get account requirements](/api-reference/accounts-v2/get-v2-accounts-requirements)
- [Find an account option](/platform/accounts/find-an-account-option)
- [Read account requirements](/platform/accounts/read-account-requirements)
