Your first Gravv integration

In this step-by-step tutorial, you create two customers, open an account for each, fund one and send a transfer to the other.

A Gravv integration starts with a customer. You create the customer, verify their identity, and open an account for them. The customer can then fund the account and send a transfer to another account.

In this tutorial, you create two customers and open an account for each. You fund one account, which this tutorial calls the source account, and transfer the funds to the other, the destination account.

Prerequisites

To complete this tutorial, you need:

  • Your API key, which you send in the Api-Key header of every request. A sandbox key (grvSec_sandbox_…) works on sandbox data, and a live key works on live data. If you don't have a key yet, follow the steps in Authentication.
  • A basic understanding of REST APIs and cURL.
  • A document to save response details in, such as a Google Doc.
  • A way to send USDC from an external wallet or exchange.
  • A new Idempotency-Key value for every POST request, such as a UUID. If you send a key again, Gravv replays the first response instead of running the request. See Idempotent requests.

All examples in these docs are sample values that do not work, including API keys, customer IDs, card IDs, wallet addresses, transaction hashes, encrypted payloads, names, emails and phone numbers. Replace them with your own values.

To see a flow like this one work before you write code, run gravv try wallet --curl with the Gravv CLI. It runs each call in sandbox and prints it as curl. To have your AI assistant follow along, connect the Gravv MCP server.

Create a customer

To create a customer, send a POST request to the Add a customer endpoint.

curl -X POST https://api.gravv.xyz/v1/customers \
  -H "Api-Key: <API_KEY>" \
  -H 'Idempotency-Key: 6d0f4c7e-1a2b-4c3d-9e8f-001122334401' \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Jane",
    "last_name": "Doe",
    "middle_name": "Sample",
    "email": "jane.doe@example.com",
    "phone": "+2348031230101",
    "type": "individual",
    "gender": "female",
    "date_of_birth": "1992-04-16",
    "address": {
      "address_line1": "Same Global Housing Estate",
      "city": "Lokogoma",
      "country": "NG",
      "postal_code": "900107",
      "state": "NG-FC"
    }
  }'

first_name and last_name are required. middle_name is optional. Send it only if it appears in the customer's verification documents. phone must be a valid number in E.164 format. The API rejects reserved test ranges such as +1555….

The response contains the customer details and their id. Save the id in your document as source account customer_id:

{
  "data": {
    "address": {
      "address_line1": "Same Global Housing Estate",
      "city": "Lokogoma",
      "country": "NG",
      "postal_code": "900107",
      "state": "NG-FC"
    },
    "date_added": "2026-10-13T09:12:44Z",
    "date_of_birth": "1992-04-16",
    "email": "jane.doe@example.com",
    "first_name": "Jane",
    "gender": "female",
    "id": "d12ded01-9081-4a3c-a76c-70654f0a0175",
    "last_name": "Doe",
    "middle_name": "Sample",
    "phone": "+2348031230101",
    "status": "active",
    "type": "individual"
  },
  "error": null
}

Initiate know your customer (KYC)

A customer must pass KYC (know your customer) identity checks before you can open an account for them. An account request for an unverified customer fails with CUSTOMER_KYC_INCOMPLETE. To start KYC, send the source account customer_id to the Start KYC endpoint:

curl --request POST \
     --url https://api.gravv.xyz/v1/customers/kyc/start \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 6d0f4c7e-1a2b-4c3d-9e8f-001122334402' \
     --header 'content-type: application/json' \
     --data '
{
  "customer_id": "d12ded01-9081-4a3c-a76c-70654f0a0175"
}
'

The response contains a verification link:

{
  "data": {
    "review_status": "pending",
    "status": "success",
    "web_url": "https://<verification-host>/websdk/p/…"
  },
  "error": null
}

Send web_url to the customer. The link is valid for one hour. At the link, the customer uploads their documents and completes a liveness check. You can also upload the documents yourself. See KYC.

Gravv sends the outcome as a customer.kyc.status.completed or customer.kyc.status.failed webhook. You can also read the outcome from the customer. Get a customer returns kyc, and the status of the basic entry becomes completed. Continue when it does.

Sandbox KYC does not complete today. The basic entry stays pending and Gravv sends no outcome webhook. Until Gravv fixes this, you can't complete the account step below in sandbox with a new customer.

Create an account for a customer

Opening an account takes three calls. First, find a USD option the customer can open with Get account capabilities:

curl --request GET \
     --url 'https://api.gravv.xyz/v1/accounts/capabilities?customer_id=d12ded01-9081-4a3c-a76c-70654f0a0175&customer_type=individual&currency=USD' \
     --header 'Api-Key: <API_KEY>'

Pick an option whose token_wallets list has polygon, and save its bank_code. That option is wallet-backed. The bank turns each deposit into USDC and puts it in a wallet that belongs to the account. See Find an account option.

Next, call Get account requirements for that bank_code. It lists the fields and documents the bank needs. See Read account requirements.

Then create the account with your source account customer_id, the bank_code, and the data the requirements list. This example sends the fields one USD option asks for:

curl --request POST \
     --url https://api.gravv.xyz/v2/accounts \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 6d0f4c7e-1a2b-4c3d-9e8f-001122334403' \
     --header 'content-type: application/json' \
     --data '
{
  "customer_id": "d12ded01-9081-4a3c-a76c-70654f0a0175",
  "customer_type": "individual",
  "currency": "USD",
  "account_type": "operating",
  "category": "sub_account",
  "bank_code": "<bank_code>",
  "network": "polygon",
  "label": "Jane Doe",
  "data": {
    "root": {
      "first_name": "Jane",
      "last_name": "Doe",
      "date_of_birth": "1992-04-16",
      "email": "jane.doe@example.com",
      "phone_daytime": "+2348031230101",
      "gender": "female",
      "citizenship_country": "NG",
      "tax_residence_country": "NG",
      "permanent_address": {
        "address_line": "Same Global Housing Estate",
        "city": "Lokogoma",
        "state": "NG-FC",
        "postal_code": "900107",
        "country": "NG"
      }
    },
    "documents": [
      {
        "document_type": "passport",
        "issuing_country": "NG",
        "number": "A12345678",
        "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": "NG",
        "filename": "utility-bill.pdf",
        "content": "<base64 of a recent utility bill>"
      }
    ]
  }
}
'

The bank reviews the account in the background, so the response has status pending and no bank details yet:

{
  "data": {
    "account_id": "a4d83e55-435e-41c9-9a0b-dcb99f690c95",
    "status": "pending"
  },
  "error": null
}

In your document, save the account_id as source account_id.

Wait until the account is active before you share its details or move funds. The applications.accounts.completed webhook tells you when this happens, or you can poll Get an account. The bank details (virtual_account_* fields) appear once the account is active. Save the account's wallet_address as source account wallet_address. See Track activation.

If the create call returns a 400, read details, which names every field to fix. Send the corrected request with a new Idempotency-Key, because Gravv replays a 4xx under its key for 24 hours. After a 5xx or a timeout, retry with the same key.

Fund the customer's account

To fund the account, send USDC on the Polygon network from an external wallet or exchange to the source account wallet_address.

Send USDC on the Polygon network, not on Ethereum or another network. How long the deposit takes depends on the network.

Confirm the customer's account balance

When the deposit arrives, it shows in the account balance. To confirm it, call the Get an account endpoint with the source account_id:

curl --request GET \
     --url https://api.gravv.xyz/v2/accounts/a4d83e55-435e-41c9-9a0b-dcb99f690c95 \
     --header 'Api-Key: <API_KEY>'

The response shows the account balance:

{
  "data": {
    "id": "a4d83e55-435e-41c9-9a0b-dcb99f690c95",
    "label": "Jane Doe",
    "status": "active",
    "currency": "USD",
    "category": "sub_account",
    "customer_id": "d12ded01-9081-4a3c-a76c-70654f0a0175",
    "wallet_address": "0xf73bdd069dc31aa8f334b177b175936ba98237a2",
    "blockchain_network": "polygon",
    "virtual_account_bank_name": "<bank name>",
    "virtual_account_bank_account_number": "900300758080",
    "virtual_account_bank_routing_number": "101019644",
    "virtual_account_bank_beneficiary_name": "Jane Doe",
    "balance": "100",
    "balance_source": "wallet",
    "application_id": "5e7a9c1b-2d4f-4a6b-8c0d-e1f2a3b4c5d6"
  },
  "error": null
}

balance is in major units, so "100" is 100 USD. See List and read accounts.

Initiate a transfer to another account

With the balance confirmed, you can send a transfer from the source account to a destination account. Any account you create with the Create an account endpoint is an internal account.

Because the destination is an internal account, you need only the source account customer_id, the source account_id and the id of the destination account:

  1. Create a destination account. Repeat Create a customer, Initiate know your customer (KYC) and Create an account for a customer. Use different personal details for this second customer, such as the name, email address and phone number, and use new idempotency keys.
  2. Save the account_id from the Create an account for a customer response as destination account_id. The destination account must be active.
  3. Have these values ready for the transfer request:
    • source account customer_id
    • source account_id
    • destination account_id
  4. Send the transfer request with these values. Send amount as a string to keep its precision. description and client_reference are optional:
curl --request POST \
     --url https://api.gravv.xyz/v1/transfer \
     --header 'Api-Key: <API_KEY>' \
     --header 'Idempotency-Key: 6d0f4c7e-1a2b-4c3d-9e8f-001122334407' \
     --header 'content-type: application/json' \
     --data '
{
  "source": {
    "source_type": "internal_account",
    "id": "a4d83e55-435e-41c9-9a0b-dcb99f690c95"
  },
  "destination": {
    "destination_type": "internal_account",
    "id": "d33981df-26d6-4905-9557-aacccd363adb"
  },
  "amount": "100",
  "description": "transfer testing",
  "customer_id": "d12ded01-9081-4a3c-a76c-70654f0a0175",
  "client_reference": "987908790"
}
'

The response has transfer_status pending and a transaction_id. The transfer then runs in the background. Follow it with the transfer.status.* webhooks or with Get a transaction.

To learn more about transfers, see Transfers.

Was this page helpful?