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
- The business's Polygon wallet holds 2,000.00 USDC.
- The business sends 500.00 USDC to the supplier's Polygon address. The transfer response gives the fee in
feeandfee_currency. Say the fee is 1.00. - 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}showsassets[]withsymbolUSDCand abalanceof 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
- 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
}
- The same network. Ask the supplier which network the address is on. The same
0xaddress exists on every EVM network, so the address alone does not tell you. - The asset. Send
source.crypto_assetasUSDCorUSDT. Do not send a differentdestination.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
transfer.status.pending, withworkflow_typecrypto_transfer, usually twice. Both events already carryevent_data.tx_hash.transfer.status.completedwithevent_data.tx_hash, ortransfer.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.