Convert USDT and USDC
Preview the price, then convert USDT to USDC or back between two wallets of the same customer.
Use this recipe to preview the price, then convert USDT to USDC, or USDC to USDT, between two wallets of the same customer.
Who it is for. A treasury app whose customer received USDT on Ethereum but needs USDC on Polygon to pay suppliers.
Status. Available between two wallets of the same customer. No conversion has completed in production yet, so always preview the price first. Gravv does not support Tron. You cannot convert and send to someone else in one call. Convert first, then send.
Money example
- The customer converts 2.00 USDC in a Polygon wallet into USDT in their Ethereum wallet.
- The preview shows a fee of 1.501737 USDC and a receive amount of 0.498263 USDT.
- 2.00 USDC leaves the Polygon wallet, and about 0.498 USDT arrives. The fee comes out of the amount that arrives. Gravv never charges it on top.
- Part of the fee works like a fixed cost, so preview the price first and convert larger amounts.
Before you start
- Both wallets are Gravv wallets (
internal_crypto_wallet) of the same customer, and that customer is thecustomer_idyou send (GET /v1/wallets/{wallet_id}showscustomer_id). - The source wallet holds the asset you send.
- The amount has at most 2 decimal places.
Find out what is required
Get a price first with GET /v1/transfer/rates in preview mode. A preview reserves and charges nothing.
curl --request GET \
--url 'https://api.gravv.xyz/v1/transfer/rates?source_wallet_id=b6705e3a-3c6c-49b0-b24f-d55aafd03635&destination_wallet_id=5485cc5d-3d4b-4b17-9033-95e408405f03&send_asset=USDC&receive_asset=USDT&amount=2.00' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/transfer/rates?source_wallet_id=b6705e3a-3c6c-49b0-b24f-d55aafd03635&destination_wallet_id=5485cc5d-3d4b-4b17-9033-95e408405f03&send_asset=USDC&receive_asset=USDT&amount=2.00", {
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/transfer/rates?source_wallet_id=b6705e3a-3c6c-49b0-b24f-d55aafd03635&destination_wallet_id=5485cc5d-3d4b-4b17-9033-95e408405f03&send_asset=USDC&receive_asset=USDT&amount=2.00",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
"data": {
"items": [
{
"currency_pair": "USDC_USDT",
"from_currency": "USDC",
"to_currency": "USDT",
"rate": "0.249132",
"direction": "USDC_USDT",
"timestamp": "2026-10-12T13:40:00Z",
"send_amount": "2",
"fee": "1.501737",
"fee_currency": "USDC",
"receive_amount": "0.498263",
"min_receive_amount": "0.498263",
"source_network": "polygon",
"destination_network": "ethereum",
"expires_at": "2026-10-12T13:41:00Z"
}
]
},
"error": null
}
receive_amount is the amount that should arrive, and min_receive_amount is the lowest amount that can arrive. fee is in the asset you send. The preview is not binding. Gravv fetches the price again when you send the transfer.
Steps
1. Convert
Send a transfer whose destination.crypto_asset differs from the asset you send. That difference makes the transfer a conversion.
curl --request POST \
--url https://api.gravv.xyz/v1/transfer \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 335c34c9-1739-428f-9bcc-478645b1c8e5' \
--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": "internal_crypto_wallet",
"id": "5485cc5d-3d4b-4b17-9033-95e408405f03",
"crypto_asset": "USDT"
},
"amount": "2.00",
"description": "Convert USDC to USDT"
}
'
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": "335c34c9-1739-428f-9bcc-478645b1c8e5",
},
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": "internal_crypto_wallet",
"id": "5485cc5d-3d4b-4b17-9033-95e408405f03",
"crypto_asset": "USDT"
},
"amount": "2.00",
"description": "Convert USDC to USDT"
}),
});
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": "335c34c9-1739-428f-9bcc-478645b1c8e5",
},
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": "internal_crypto_wallet",
"id": "5485cc5d-3d4b-4b17-9033-95e408405f03",
"crypto_asset": "USDT",
},
"amount": "2.00",
"description": "Convert USDC to USDT",
},
)
print(response.status_code, response.json())
{
"data": {
"amount": "2",
"amount_currency": "USDC",
"reference": "f3a9c1d7-2b4e-4c6a-9d8f-0e1b2c3d4e5f",
"date_created": "2026-10-12T13:42:10.000Z",
"transfer_status": "pending",
"transaction_id": "a7c3e9f1-4d2b-4a8c-b6e0-1f2a3b4c5d6e",
"local_amount": null,
"local_currency": null,
"fee": "1.501737",
"fee_currency": "USDC",
"requires_approval": false,
"receive_amount": "0.498263",
"min_receive_amount": "0.498263",
"receive_asset": "USDT"
},
"error": null
}
Compare receive_amount with the preview, and check that it is still acceptable to you.
2. Wait for it
Wait for the transfer.status.completed webhook. It gives the amount that arrived (receive_amount) and the amount the converter kept (provider_fee). As a backup, poll GET /v1/transactions/a7c3e9f1-4d2b-4a8c-b6e0-1f2a3b4c5d6e.
The transaction shows no fee for a conversion. You find the fee in the transfer response and the webhooks.
3. Pay out
Send the converted funds from the receiving wallet as in Send stablecoins to an outside wallet.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 Both wallets must belong to the same customer to convert USDT to USDC. |
The wallets belong to different customers, the destination is not a Gravv wallet, or customer_id is not the wallets' owner |
Convert between one customer's wallets, then send to others |
400 Only USDT and USDC can be converted. |
You sent another asset | Use USDT or USDC |
400 Converting USDT on polygon to USDC on solana isn't available. |
Gravv does not offer that route right now | Try another pair of networks |
400 This wallet can't be used for conversions yet. |
The wallet is not set up for conversions | Use another wallet, or contact support |
400 The amount is too small to convert. Try a larger amount. |
The fee would take the whole amount | Convert a larger amount |
400 The amount is too large to convert. Try a smaller amount. |
The amount is above the converter's limit | Split it into smaller conversions |
400 This amount can't be converted. Try a different amount. |
The converter refused the amount | Change the amount |
400 We couldn't get a price right now. Please try again shortly. |
No price is available | Retry later with a new idempotency key |
400 Use at most 2 decimal places. |
The amount has more than 2 decimal places, such as 2.005 |
Round to 2 decimal places |
400 receive_asset must differ from the asset sent. (preview) |
Both sides use the same asset | Pick the other asset |
400 source_wallet_id and destination_wallet_id are required to price a conversion. (preview) |
A wallet id is missing | Send both |
transfer.status.failed with refunded_amount and refund_asset in event_data |
The conversion did not go through. The asset came back, possibly minus the amount the converter kept (provider_fee) |
Check the refund in the source wallet, then retry |
Webhooks you will receive
transfer.status.pending, withworkflow_typestablecoin_conversion. Itsevent_datacarriessend_asset,amount_sent,receive_asset,receive_amount,min_receive_amountandprovider_fee.- Then one of these:
transfer.status.completed, with the actualreceive_amountandprovider_fee.transfer.status.failed, withrefunded_amountandrefund_asset.
Testing in sandbox
You cannot test a conversion in sandbox. There is no test converter, so both the preview and the conversion return We couldn't get a price right now. Please try again shortly.
The request checks still run first. So in sandbox you can still see receive_asset must differ from the asset sent., Use at most 2 decimal places. and the same-customer error.