Send stablecoins to an outside wallet

Send USDC or USDT from a Gravv wallet to an outside address on the same network, including Stellar addresses that need a memo.

Use this recipe to send USDC or USDT from a Gravv wallet to an outside address on the same network, including a Stellar address that needs a memo.

Who it is for. An import business that pays a supplier's USDC address on Polygon, and sometimes withdraws to a crypto exchange's Stellar address that needs a memo.

Status. Available on the same network. Not supported from avalanche or sui wallets, or on tron.

Money example

  1. The business's Polygon wallet holds 2,000.00 USDC.
  2. The business sends 500.00 USDC to the supplier's Polygon address. The transfer response gives the fee in fee and fee_currency. Say the fee is 1.00.
  3. The supplier receives 500.00 USDC, and the wallet balance drops by 501.00.

Before you start

  • The source wallet has the balance. GET /v1/wallets/{wallet_id} shows assets[] with symbol USDC and a balance of at least the amount plus the fee. A missing asset means Gravv has not read the balance yet, not that the balance is zero.
  • The wallet is not on avalanche or sui, because wallets on those networks cannot send.
  • You have the destination address and its network, and, for an exchange, its memo.

Find out what is required

  1. The customer's wallets and networks. List them with their balances:
curl --request GET \
 --url 'https://api.gravv.xyz/v1/wallets?customer_id=c7ae0522-6a4d-476f-a43c-5a9aac863c31&include_balances=true' \
 --header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/wallets?customer_id=c7ae0522-6a4d-476f-a43c-5a9aac863c31&include_balances=true", {
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/v1/wallets?customer_id=c7ae0522-6a4d-476f-a43c-5a9aac863c31&include_balances=true",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
  "data": {
    "items": [
      {
        "id": "b6705e3a-3c6c-49b0-b24f-d55aafd03635",
        "address": "0x2fa8509655a24d40634fdac5fc0f5468025c13b6",
        "network": "polygon",
        "name": "Supplier payouts",
        "date_created": "2026-09-02T08:14:03Z",
        "customer_id": "c7ae0522-6a4d-476f-a43c-5a9aac863c31",
        "wallet_type": "customer",
        "assets": [
          {
            "symbol": "USDC",
            "balance": "2000.000000",
            "decimals": 6,
            "updated_at": "2026-10-13T08:55:00Z"
          },
          {
            "symbol": "USDT",
            "balance": "0.000000",
            "decimals": 6,
            "updated_at": "2026-10-13T08:55:00Z"
          }
        ]
      }
    ],
    "page": 1,
    "items_per_page": 10,
    "total_items": 1,
    "total_pages": 1,
    "has_next_page": false,
    "has_previous_page": false
  },
  "error": null
}
  1. The same network. Ask the supplier which network the address is on. The same 0x address exists on every EVM network, so the address alone does not tell you.
  2. The asset. Send source.crypto_asset as USDC or USDT. Do not send a different destination.crypto_asset, because that makes the transfer a conversion. See Convert USDT and USDC.

Steps

1. Send

curl --request POST \
 --url https://api.gravv.xyz/v1/transfer \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: b9d76632-1364-4759-ae26-3e31b70a4314' \
 --data '
{
"customer_id": "c7ae0522-6a4d-476f-a43c-5a9aac863c31",
"source": {
"source_type": "internal_crypto_wallet",
"id": "b6705e3a-3c6c-49b0-b24f-d55aafd03635",
"crypto_asset": "USDC"
},
"destination": {
"destination_type": "crypto_wallet",
"wallet_address": "0xdf19d8699ab5aef6fb2d6cd81c18c86fe4b21475",
"blockchain_network": "polygon"
},
"amount": "500.00",
"description": "PO 7781",
"client_reference": "imp-po-7781"
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "b9d76632-1364-4759-ae26-3e31b70a4314",
},
body: JSON.stringify({
"customer_id": "c7ae0522-6a4d-476f-a43c-5a9aac863c31",
"source": {
  "source_type": "internal_crypto_wallet",
  "id": "b6705e3a-3c6c-49b0-b24f-d55aafd03635",
  "crypto_asset": "USDC"
},
"destination": {
  "destination_type": "crypto_wallet",
  "wallet_address": "0xdf19d8699ab5aef6fb2d6cd81c18c86fe4b21475",
  "blockchain_network": "polygon"
},
"amount": "500.00",
"description": "PO 7781",
"client_reference": "imp-po-7781"
}),
});
console.log(response.status, await response.json());
import os

import requests

response = requests.post(
"https://api.gravv.xyz/v1/transfer",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
    "Content-Type": "application/json",
    "Idempotency-Key": "b9d76632-1364-4759-ae26-3e31b70a4314",
},
json={
    "customer_id": "c7ae0522-6a4d-476f-a43c-5a9aac863c31",
    "source": {
        "source_type": "internal_crypto_wallet",
        "id": "b6705e3a-3c6c-49b0-b24f-d55aafd03635",
        "crypto_asset": "USDC",
    },
    "destination": {
        "destination_type": "crypto_wallet",
        "wallet_address": "0xdf19d8699ab5aef6fb2d6cd81c18c86fe4b21475",
        "blockchain_network": "polygon",
    },
    "amount": "500.00",
    "description": "PO 7781",
    "client_reference": "imp-po-7781",
},
)
print(response.status_code, response.json())
{
  "data": {
    "amount": "500",
    "amount_currency": "USD",
    "date_created": "2026-10-13T14:00:01.410Z",
    "fee": "0",
    "fee_currency": "USD",
    "local_amount": null,
    "local_currency": null,
    "reference": "47afd450-561e-40a6-b658-8e96c180eb26",
    "requires_approval": false,
    "transaction_id": "b937af87-991a-4ca0-bb59-e9eda2bf6f7e",
    "transfer_status": "pending"
  },
  "error": null
}

In the response, amount_currency is USD, not USDC. The source can also be a wallet-backed account ("source_type": "internal_account" with the account id). Its network is the account's blockchain_network.

2. To an exchange's Stellar address, add the memo

From a Stellar wallet, add blockchain_memo exactly as the exchange shows it. Without it, the exchange cannot tell whose deposit it is.

curl --request POST \
 --url https://api.gravv.xyz/v1/transfer \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: fd39e5c6-1b73-4f61-bb6f-7832523531e6' \
 --data '
{
"customer_id": "c7ae0522-6a4d-476f-a43c-5a9aac863c31",
"source": {
"source_type": "internal_crypto_wallet",
"id": "81194af2-92eb-41cf-9667-94e26d069aa3",
"crypto_asset": "USDC"
},
"destination": {
"destination_type": "crypto_wallet",
"wallet_address": "GCEKNUKI5N5TWQVPA7YHUSCWQPYPI2CHUUSB2Y4J2R2RLNWUMJG3LMYQ",
"blockchain_network": "stellar",
"blockchain_memo": "4820176"
},
"amount": "250.00",
"client_reference": "imp-xlm-withdraw-0031"
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "fd39e5c6-1b73-4f61-bb6f-7832523531e6",
},
body: JSON.stringify({
"customer_id": "c7ae0522-6a4d-476f-a43c-5a9aac863c31",
"source": {
  "source_type": "internal_crypto_wallet",
  "id": "81194af2-92eb-41cf-9667-94e26d069aa3",
  "crypto_asset": "USDC"
},
"destination": {
  "destination_type": "crypto_wallet",
  "wallet_address": "GCEKNUKI5N5TWQVPA7YHUSCWQPYPI2CHUUSB2Y4J2R2RLNWUMJG3LMYQ",
  "blockchain_network": "stellar",
  "blockchain_memo": "4820176"
},
"amount": "250.00",
"client_reference": "imp-xlm-withdraw-0031"
}),
});
console.log(response.status, await response.json());
import os

import requests

response = requests.post(
"https://api.gravv.xyz/v1/transfer",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
    "Content-Type": "application/json",
    "Idempotency-Key": "fd39e5c6-1b73-4f61-bb6f-7832523531e6",
},
json={
    "customer_id": "c7ae0522-6a4d-476f-a43c-5a9aac863c31",
    "source": {
        "source_type": "internal_crypto_wallet",
        "id": "81194af2-92eb-41cf-9667-94e26d069aa3",
        "crypto_asset": "USDC",
    },
    "destination": {
        "destination_type": "crypto_wallet",
        "wallet_address": "GCEKNUKI5N5TWQVPA7YHUSCWQPYPI2CHUUSB2Y4J2R2RLNWUMJG3LMYQ",
        "blockchain_network": "stellar",
        "blockchain_memo": "4820176",
    },
    "amount": "250.00",
    "client_reference": "imp-xlm-withdraw-0031",
},
)
print(response.status_code, response.json())

3. Wait for the chain

Poll GET /v1/transactions/b937af87-991a-4ca0-bb59-e9eda2bf6f7e until its status is completed or failed. A completed transaction has tx_hash. To find a transfer by its hash later, use GET /v1/transactions?tx_hash=….

What can go wrong

Response Why Fix
400 the source account (polygon network) and destination (ethereum network) must be on the same blockchain network The networks differ Use a wallet on the destination's network
400 transfers from avalanche accounts are not currently supported (also sui) Wallets on these networks cannot send Move the funds to another network first
400 insufficient balance from a tron or arbitrum wallet These wallets have no tracked balance, so they cannot send Send from a polygon, ethereum, solana or stellar wallet
400 destination.wallet_address is required for a crypto wallet transfer or destination.blockchain_network is required for a crypto wallet transfer A required field is missing Send both
400 Both wallets must belong to the same customer to convert USDC to USDT. destination.crypto_asset differs from the asset sent Leave destination.crypto_asset out. To send the other asset, convert first
400 the source wallet was not found or the source wallet is not active (status: …) The wallet id is wrong Find the right id with GET /v1/wallets
400 insufficient balance The amount plus the fee is more than the balance Fund the wallet
Accepted, then transfer.status.failed soon after with no tx_hash amount had more than 2 decimal places Send at most 2 decimal places
Accepted, then transfer.status.failed about 30 minutes later, and the fee refunded The network refused the send because of a malformed address, an unsupported asset, or a Stellar address that has not enabled USDC. Gravv does not check the format of same-network addresses when you send Fix the address, or ask the receiver to enable USDC on Stellar, then send again
pending with no progress Gravv is still waiting for the chain's response Wait. If it lasts unusually long, contact support with the reference
Sent to an exchange without the memo The funds reached the exchange's shared address Contact the exchange with the tx_hash. You cannot recall an on-chain send

Webhooks you will receive

  1. transfer.status.pending, with workflow_type crypto_transfer, usually twice. Both events already carry event_data.tx_hash.
  2. transfer.status.completed with event_data.tx_hash, or transfer.status.failed.

Testing in sandbox

A normal transfer with a sandbox key settles at once with sandbox test dollars. For example, 2.00 USDC from a polygon wallet to an outside polygon address completed with a tx_hash within 3 seconds (channel polygon, transaction_type crypto_transfer).

Sandbox wallets are on test networks, and public faucet USDC does not show in their balance. A sandbox Stellar wallet could not be funded, so the memo send returns insufficient balance after its request checks pass.

Reference

Was this page helpful?