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.

  1. The client sends 1,200.00 USD by ACH to Ada's account number and routing number.
  2. The bank converts the payment to USDC and delivers it to the account's wallet on polygon.
  3. 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 basic KYC is completed.
  • 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 with GET /v2/accounts?customer_id=…&currency=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_action is true, go to Unblock an application.
  • If status_reason is provider_review or provisioning_retrying, you have nothing to do. Wait.
  • If status is failed or rejected, read status_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:

  1. applications.accounts.pending. Gravv can skip this event.
  2. Sometimes applications.accounts.blocked, with metadata.block_reason.
  3. applications.accounts.completed with account_id, or applications.accounts.failed with metadata.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.

Reference

Was this page helpful?