Open a wallet-backed USD account for a person
Open a USD account with US bank details for an individual, whose deposits arrive as USDC in the account's wallet.
Open a USD account with US bank details for an individual. Deposits to the account arrive as USDC in the account's wallet.
API version: v2.
Who it is for. This recipe is for a freelancer marketplace that pays UK and EU freelancers in dollars. It wants each freelancer's USD income to arrive as USDC that it can move at once.
Status. Available for individuals in the countries each option serves. polygon works on every option. stellar, ethereum, avalanche_c_chain and solana work where the option's token_options lists them. Gravv supports no other network.
Money example
Ada, a UK freelancer, invoices a US client 1,200.00 USD.
- The client sends 1,200.00 USD by ACH to Ada's account number and routing number.
- The bank converts the payment to USDC and delivers it to the account's wallet on polygon.
- Gravv books the full deposit and sends
transfer.inflow.deposit_settled. If your plan has a deposit fee for that rail, Gravv bills it to your business separately.
Before you start
- Ada's
basicKYC iscompleted. - Capabilities for her list a USD option with a non-empty
token_wallets. See Find what a customer can open. - She has not reached
max_accounts_per_customer. Count her accounts withGET /v2/accounts?customer_id=…¤cy=USD.
Find out what is required
Read the requirements for the option's bank_code. The forms differ by option:
| The default USD option | The USD, EUR and GBP option | The EUR and USD option | |
|---|---|---|---|
| Root fields | first_name, last_name, date_of_birth, email, phone_daytime, gender, citizenship_country, tax_residence_country, permanent_address |
Names, date_of_birth, email, citizenship_country, permanent_address, and expected_activity (expected_avg_daily_balance, ach_in.monthly_count, ach_in.monthly_amount) |
Names, date_of_birth, email, phone_daytime, citizenship_country, tax_residence_country, permanent_address, source_of_income, employment_status, work_industry, annual_deposit_range, transaction_frequency |
| US citizens only | tax_id_type (ssn) and tax_id_number |
tax_id_type (ssn) once a tax_id_number is sent |
None |
| Non-US citizens only | None | immigration_status, tax_residence_country, and international_supplement (four yes/no questions, with details when one is true) |
None |
| Documents | One identity document and proof_of_address |
The same | The same |
| Networks that work | polygon, ethereum, solana |
polygon, ethereum, solana, stellar |
polygon |
permanent_address holds address_line, city, state (ISO 3166-2, such as GB-LND), postal_code and country. It must not be a P.O. Box. An identity document is one of national_id, passport, drivers_license, permanent_resident_card, employment_authorization or other, with number, issued_date, expiration_date and issuing_country. If the requirements response differs from this table, trust the requirements response.
Steps
1. Open the account
curl --request POST \
--url https://api.gravv.xyz/v2/accounts \
--header 'Api-Key: <API_KEY>' \
--header 'Idempotency-Key: b6fb7a32-2a3e-4043-8255-ab239c331f1d' \
--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>"
}
]
}
}
'
const response = await fetch("https://api.gravv.xyz/v2/accounts", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Idempotency-Key": "b6fb7a32-2a3e-4043-8255-ab239c331f1d",
"Content-Type": "application/json",
},
body: JSON.stringify({
"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>"
}
]
}
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v2/accounts",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Idempotency-Key": "b6fb7a32-2a3e-4043-8255-ab239c331f1d",
"Content-Type": "application/json",
},
json={
"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>",
},
],
},
},
)
print(response.status_code, response.json())
{
"data": {
"account_id": "6f1c2a9e-3b1d-4f7a-9c62-0d8e4b7a5c11",
"status": "pending"
},
"error": null
}
Save account_id. The response has no application_id.
To back the account with a wallet you already hold, send wallet_id instead of network. The wallet's network must be one the option offers.
2. Follow it until it is active
curl --request GET \
--url https://api.gravv.xyz/v2/accounts/6f1c2a9e-3b1d-4f7a-9c62-0d8e4b7a5c11 \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v2/accounts/6f1c2a9e-3b1d-4f7a-9c62-0d8e4b7a5c11", {
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/6f1c2a9e-3b1d-4f7a-9c62-0d8e4b7a5c11",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
"data": {
"id": "6f1c2a9e-3b1d-4f7a-9c62-0d8e4b7a5c11",
"status": "pending",
"status_reason": "provider_review",
"status_message": "The bank is reviewing this application. Nothing is outstanding from you or the customer.",
"requires_user_action": false,
"currency": "USD",
"category": "sub_account",
"blockchain_network": "polygon",
"application_id": "5e7a9c1b-2d4f-4a6b-8c0d-e1f2a3b4c5d6"
},
"error": null
}
- If
requires_user_actionistrue, go to Unblock an application. - If
status_reasonisprovider_revieworprovisioning_retrying, you have nothing to do. Wait. - If
statusisfailedorrejected, readstatus_reason. See what each one means.
Or wait for applications.accounts.completed. Poll every few seconds at first, then less often, because a bank review can take minutes or days.
3. Give the details to the payer
When status is active, read the account again and show the virtual_account_bank_* fields. See Share account details and receive a deposit.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 the customer for the customer_id supplied has not completed KYC; … (CUSTOMER_KYC_INCOMPLETE) |
KYC is not completed. The API checks KYC first and reports nothing else until it passes | Finish KYC, then retry with a new key |
400 no customer record exists for the customer_id supplied; … (CUSTOMER_NOT_FOUND) |
The id is wrong, or it is a sandbox id sent with a live key | Send the id from the same environment |
400 network is required for account options that support wallet integration |
You sent no network or wallet_id |
Send network |
400 this account does not provide a token wallet on bitcoin; choose one of its supported networks (primary: polygon) |
The option does not offer that network | Send one from token_options |
400 Network must be sent as a top-level request field; data.root.network is no longer accepted |
You sent network in data.root |
Move it to the top level |
400 invalid request payload: unknown field "blockchain_network" |
You used an old field name | Send network |
400 wallet_id and network are mutually exclusive; send only one |
You sent both | Send only one |
400 wallet_id does not reference a known wallet |
The wallet is not one of yours | Check with GET /v1/wallets |
400 Date of Birth is required (and 1 other field; see details) |
Fields are missing. details lists all of them |
Fill every key in details, then retry with a new key |
400 details.documents: required: requires document proof_of_address |
A document is missing | Add it |
400 Expiration Date is required for this document type |
An ID is missing its details | Add number, issued_date, expiration_date, issuing_country |
400 document content too large: maximum size is 2MB; … |
A file is over 2 MB | Compress it |
413 the uploaded file is too large — the maximum is 10MB |
In sandbox, a request of about 600 KB or more, whatever the message says | Keep the files of one request under 500 KB together |
400 content must be base64 (standard or URL-safe) |
The content has a data: prefix or bad base64 |
Send raw base64 |
400 details.phone_daytime: invalid_format: must be a valid phone number for its country, … |
The number is not valid for its country | Send it in E.164 format |
400 details.permanent_address.address_line: must be a physical address, not a P.O. Box |
The address is a P.O. Box | Send a street address |
400 state "LND" is not an ISO-3166-2 subdivision; … |
The state has no country prefix | Send the full code, such as GB-LND |
400 details.gender: invalid_enum: … expected one of: female, male |
These forms do not accept other |
Ask the customer, or choose another option |
400 parties are not accepted for an individual customer; … |
You sent parties for a person |
Remove parties |
409 this customer already holds the maximum number of USD accounts permitted for this account type (5) |
The customer reached the per-customer limit | Open a second account |
409 customer already has an application in progress for this bank |
An earlier application at this bank is open | Unblock that application |
503 Service temporarily unavailable. …, 500, 504 |
A temporary fault | Retry with the same key |
Account read: status_reason settlement_wallet_unavailable |
Gravv could not create the wallet on that network | Open a new account on another network |
Webhooks you will receive
Gravv sends these events in order:
applications.accounts.pending. Gravv can skip this event.- Sometimes
applications.accounts.blocked, withmetadata.block_reason. applications.accounts.completedwithaccount_id, orapplications.accounts.failedwithmetadata.fail_reason.
Gravv does not send account.activated. See Account application events.
Testing in sandbox
Sandbox calls the bank's own test system, so activation is neither instant nor certain. Keep the documents of one request under 500 KB together, because sandbox refuses requests of about 600 KB or more. Sandbox KYC does not complete today, so the create call returns CUSTOMER_KYC_INCOMPLETE for now. Once an account is active, simulate a deposit.