Open an account
Open a v2 account with one POST /v2/accounts, and read the errors it can return.
API version: v2.
POST /v2/accounts opens an account at the option you chose, with the data its requirements form asks for. There is no draft, submit or process step. The one call starts the bank's review.
A money example. Ada's account for her 1,200.00 USD invoice is a wallet-backed USD account on polygon. You send one request with her details, her passport and a utility bill. The response has status pending. A few minutes or days later the bank approves the account, and her client can pay.
Before you start
- The customer's
basicKYC iscompleted. The API checks this first. Until it passes, the API reports no other error. - You have an option from capabilities and its form from requirements.
- The customer has not reached
max_accounts_per_customerat that option. To count the customer's accounts, useGET /v2/accounts?customer_id=….
Send the request
curl --request POST \
--url https://api.gravv.xyz/v2/accounts \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: 5eedf850-57d4-4d57-ad69-6f0593c45c2b' \
--header 'Content-Type: application/json' \
--data '
{
"customer_id": "0b6f3c1e-2a4d-4c8e-9f10-7d2a6b5e4c33",
"customer_type": "individual",
"currency": "USD",
"account_type": "operating",
"category": "sub_account",
"bank_code": "<bank_code>",
"network": "polygon",
"label": "Ada USD",
"data": {
"root": {
"first_name": "Ada",
"last_name": "Okafor",
"date_of_birth": "1991-03-14",
"email": "ada.okafor@example.com",
"phone_daytime": "+447911123456",
"gender": "female",
"citizenship_country": "GB",
"tax_residence_country": "GB",
"permanent_address": {
"address_line": "12 Crown Street",
"city": "London",
"state": "GB-LND",
"postal_code": "EC1A 1BB",
"country": "GB"
}
},
"documents": [
{
"document_type": "passport",
"issuing_country": "GB",
"number": "533401372",
"issued_date": "2022-01-31",
"expiration_date": "2032-01-30",
"filename": "passport.jpg",
"content": "<base64 of the passport photo page>"
},
{
"document_type": "proof_of_address",
"issuing_country": "GB",
"filename": "utility-bill.pdf",
"content": "<base64 of a recent utility bill>"
}
]
}
}
'
{
"data": {
"account_id": "6f1c2a9e-3b1d-4f7a-9c62-0d8e4b7a5c11",
"status": "pending"
},
"error": null
}
Save account_id. The response has no application_id, so read it from GET /v2/accounts/{id}.
What to send
- For a wallet-backed option (non-empty
token_wallets), sendnetworkfrom the option'stoken_options. Instead ofnetwork, you can send thewallet_idof one of your wallets on one of those networks. Never send both. - For a fiat-only option, send neither
networknorwallet_id. - For a customer's account, send
categorysub_account. For NGN, always sendcategory. - Send
customer_type. Indata.root, sendcitizenship_countryfor a person orincorporation_countryfor a business. - For a business, send
data.parties[]. Name each person from the customer record byid, and add the facts the record lacks (ownership, control, signer, documents). If you leavepartiesout, the API declares every associated person without those facts, and the request fails validation on them. - Identity documents carry
number,issued_date,expiration_dateandissuing_country.contentis raw base64, not adata:URI. - Each document is at most 2 MB. In sandbox, keep the files in one request under 500 KB in total. Sandbox refuses requests from about 600 KB with a 413.
- Gravv does not deduplicate a request body over 1 MiB under its
Idempotency-Key. Documents of about 770 KB or more make a body that large. A retry of such a request runs again instead of returning the first response. After a timeout, list the customer's accounts before you send it again.
Errors
Every 400 lists all the problems it found in details, keyed by field. Fix them all, then send the request again with a new Idempotency-Key. Gravv stores a 4xx response under its key for 24 hours and replays it if you reuse the key.
| Response | Why | Fix |
|---|---|---|
400 the customer for the customer_id supplied has not completed KYC; … (CUSTOMER_KYC_INCOMPLETE) |
basic KYC is not completed |
Finish KYC, then send again with a new key |
400 no customer record exists for the customer_id supplied; … (CUSTOMER_NOT_FOUND) |
Wrong id, or an id from the other environment | Use the id from POST /v1/customers in the same environment |
400 Date of Birth is required (and 1 other field; see details) |
Missing fields, each named in details |
Fill every key in details |
400 network is required for account options that support wallet integration |
Wallet-backed option without network or wallet_id |
Send network |
400 network is only accepted for account options that support wallet integration |
network sent to a fiat-only option |
Remove it |
400 this account does not provide a token wallet on bitcoin; … (SELECTION_CONFLICT) |
The option does not offer that network | Use one of its token_options |
400 Network must be sent as a top-level request field; data.root.network is no longer accepted |
network inside data.root |
Move it to the top level |
400 invalid request payload: unknown field "blockchain_network" |
An old field name | Send network |
400 wallet_id and network are mutually exclusive; send only one |
Both sent | Send one |
400 bank_code=<code> does not open accounts for customers resident in XX. … (COUNTRY_NOT_SERVED) |
The option does not serve that country | Choose another option |
400 bank_code=<code> does not offer currency=GBP; … (CURRENCY_NOT_SUPPORTED) |
The option does not offer that currency | Choose another option |
400 details.documents: required: requires document proof_of_address |
A required document is missing | Add it to data.documents |
400 document content too large: maximum size is 2MB; … |
A file over 2 MB | Compress it |
400 unsupported file type "image/heic"; allowed types are JPEG, PNG, and PDF |
Wrong format | Convert it |
400 content must be base64 (standard or URL-safe) |
A data: prefix or bad base64 |
Send raw base64 |
400 details.gender: invalid_enum: … expected one of: female, male |
These forms do not accept other |
Ask the customer, or choose another option |
400 state "LND" is not an ISO-3166-2 subdivision; … |
A bare state code | Send the full code, such as GB-LND or US-CA |
400 parties are not accepted for an individual customer; … |
parties on an individual |
Remove parties |
400 customer_type=business does not match this customer's registered profile, which is individual. … |
Wrong customer_type |
Send the customer's type |
400 this customer already has an existing reference at the resolved bank; root-level fields are no longer accepted for it … |
The bank already knows this customer | Send no data.root |
409 this customer already holds the maximum number of USD accounts permitted for this account type (5) |
The per-customer limit | Reuse an account, or ask support to raise the limit |
409 customer already has an application in progress for this bank |
An earlier application at this bank is open | Fix that application |
409 this account provider is currently disabled for this account |
The option is switched off for your business or this customer | Choose another option, or contact support |
422 idempotency_key_reused |
The same key with a different body | Use a new key for each different request |
503 Service temporarily unavailable. …, 500, 504 |
A temporary fault | Retry with the same key. Gravv does not open a second account |
Webhooks
Gravv sends applications.accounts.pending, then possibly applications.accounts.blocked, then applications.accounts.completed or applications.accounts.failed. Gravv never sends account.activated for a v2 account you open. See Track activation.