Transfers
Move money between your customers' accounts, to bank accounts and mobile wallets, and to crypto wallets.
One endpoint, POST /v1/transfer, sends every kind of transfer. You send where the money comes from (source), where it goes (destination) and how much (amount). The API responds at once with transfer_status pending. The transfer then runs in the background, and you follow it with webhooks or by reading the transaction.
The payee holds the recipient's details, POST /v1/transfer sends the money, and the webhooks report each change of status.
Example. Your customer's USD account holds 10,000.00 USD. You send 2,500.00 USD to a contractor's US checking account by ACH. The response shows pending and the fee, for example 1.50 USD. Gravv debits the account 2,501.50 USD and sends a transfer.status.completed webhook when the bank confirms the payment.
Move money between two accounts your customers hold at Gravv.
Pay US bank accounts in USD by ACH, wire or RTP.
Pay banks abroad by SWIFT, SEPA or Faster Payments, and local bank accounts and mobile wallets in local currency.
Send USDC or USDT to a wallet, move USDC between networks, and convert USDT and USDC.
Handle a transfer that waits for a document, sender details or a limit decision.
Read a sender's caps and what is left before you send.
Sources and destinations
source.source_type sets where the money comes from:
source_type |
What it is | Notes |
|---|---|---|
internal_account |
A customer's account at Gravv | A bank-held account pays only in its own currency, and only to a bank account or another account at the same bank. A wallet-backed account holds stablecoin and converts when it pays out |
internal_crypto_wallet |
A customer's Gravv crypto wallet | Send source.crypto_asset (USDC or USDT). Without it, tron wallets send USDT and every other network sends USDC |
card |
A customer's card | Pays only its own card account |
destination.destination_type sets where the money goes:
destination_type |
Required fields | Use it for |
|---|---|---|
external_account |
id of an active payee, and rail where the payee type needs one |
A bank account or mobile wallet outside Gravv. See External accounts |
internal_account |
id |
Another account at Gravv |
internal_crypto_wallet |
id, optional crypto_asset |
Another Gravv wallet. A different crypto_asset between two wallets of the same customer converts USDT and USDC |
crypto_wallet |
wallet_address, blockchain_network, optional blockchain_memo |
An outside crypto address |
card |
id |
A card's own account |
Rails
rail goes inside destination. The API ignores a top-level rail.
rail |
Payee type | Currency | Use it for |
|---|---|---|---|
ach |
ach_wire |
USD | US bank accounts, 1 to 3 business days |
wire |
ach_wire |
USD | US domestic wires, same day. Not an international wire |
rtp |
ach_wire |
USD | Instant US payments, from bank-held accounts only |
swift |
swift |
USD | Banks abroad that receive USD |
sepa |
sepa |
EUR | Euro IBANs in SEPA countries |
fps |
fps |
GBP | UK sort code and account number |
leave it out, or globalbank |
bank_account, mobile_money |
Local currency | Local bank accounts and mobile wallets |
Payments and supported countries lists the routes that are open today.
Optional fields
| Field | What it does |
|---|---|
description |
Your note. On payouts from bank-held accounts, Gravv sends it to the bank as the purpose of payment, so say what the money is for |
client_reference |
Your own id for the payout, 1 to 100 characters. Send it every time, so you can find the payout before a retry and do not pay it twice |
client_metadata |
Your data (often JSON text), returned on the transaction |
payment_rail_reference |
A note for the receiving bank, at most 140 plain characters (no emoji or accented letters). For ACH, at most 10 letters, digits or spaces. For wire, at most 35 characters from letters, digits, spaces and / - ? : ( ) . , ' +. Other rails accept the field but do not send it. Gravv passes it to the bank on ACH and wire payouts from wallet-backed accounts. The recipient's bank decides whether their statement shows it |
supporting_document_ids |
Up to 5 uploaded documents, such as an invoice for a SWIFT payout. See Paused transfers |
additional_information |
Sender details for routes that need them (remitter_full_name, remitter_date_of_birth, remitter_address, remitter_country and more). Gravv uses them ahead of the customer profile |
Send amount as a string with at most 2 decimal places, such as "2500.00". The API accepts a JSON number, but a number can lose precision. source.id and customer_id must be UUID version 4.
Read the response
The API returns HTTP 200 for both outcomes below, so always read transfer_status.
{
"data": {
"amount": "2500",
"amount_currency": "USD",
"reference": "3f0c6a8e-6b1d-4c1e-9a55-0d2f7b9e4a11",
"date_created": "2026-10-13T09:15:22.481Z",
"transfer_status": "pending",
"transaction_id": "8d4e2b6c-1f3a-4e7b-b0c9-5a6d7e8f9a01",
"local_amount": null,
"local_currency": null,
"fee": "1.5",
"fee_currency": "USD",
"requires_approval": false
},
"error": null
}
pending: Gravv accepted the transfer and it is running. Storereferenceandtransaction_id.referenceis the transfer id. You use it to resume a paused transfer, and webhooks carry it asevent_group_id. You usetransaction_idto read the transaction.paused: Gravv accepted the transfer, but it waits for you. Readrequirementsand see Paused transfers.local_amountandlocal_currencyare the amount priced for the recipient when there is a currency exchange. They arenullwhen there is none.feeis the fee priced for this transfer. The transaction records what Gravv charged. Read it with Get a transaction.
Gravv does not lock payout rates. It takes the rate when you send, and POST /v1/fx/quote does not lock a payout rate either. GET /v1/transfer/rates returns indicative rates. Check the timestamp on each row, because a cached row can be old.
Follow it to the end
Webhooks carry the transfer reference as event_group_id:
| Event | Meaning |
|---|---|
transfer.status.pending |
The transfer is still moving. Gravv sends one per internal step, so several arrive. Treat them all as "working" |
transfer.status.completed |
The transfer is paid |
transfer.status.failed |
Read event_data.transfer_status. failed or rejected is final, and Gravv returns the money and fee to the source. manual_processing_required is not final. A person is checking the transfer, the transaction stays pending, and you must not send it again |
transfer.status.returned |
The bank sent the money back after completed. Gravv adds a refund transaction |
transfer.requires_information |
The transfer paused |
transfer.resumed |
A paused transfer resumed |
Deduplicate on the body's event_id, never on the X-Event-ID header, because every event of one transfer shares that header. Order one transfer's events by the number after the colon in idempotency_key (<reference>:<sequence>).
As a backup, poll GET /v1/transactions/{transaction_id} every 30 to 60 seconds. Gravv never fails a payout only because time has passed. A payout the bank has not responded to stays pending. No endpoint cancels a transfer.
Errors
Most refusals are HTTP 400, with the reason in error and no code. Only some errors carry a code (limits, duplicates, documents), so match on the message text.
Gravv stores a 4xx response under its Idempotency-Key for 24 hours and replays it, so use a new key after you fix a request.
After a 5xx or a timeout, look up the payout before you retry. List the customer's newest transactions with GET /v1/transactions?customer_id=<id> and match your client_reference yourself, because the list has no client_reference filter.
Discovery endpoints
These endpoints describe what the API knows. None of them tells you whether a payout route is open.
| Endpoint | Returns |
|---|---|
GET /v1/transfer/supported-currencies |
The currency pairs that Gravv can price (currency_pair, from_currency, to_currency and their names) |
GET /v1/transfer/supported-countries |
The 91 countries the API knows, with iso_code, iso3_code and currency_code. iso_code is a substring filter |
GET /v1/transfer/supported-countries-for-address |
Countries with their state codes, for address.state and remitter_address |
GET /v1/transfer/rates |
Indicative rates by received currency, or a USDT and USDC conversion preview |
curl --request GET \
--url https://api.gravv.xyz/v1/transfer/supported-currencies \
--header 'Api-Key: <API_KEY>'
{
"data": {
"items": [
{
"currency_pair": "USD_NGN",
"from_currency": "USD",
"from_currency_name": "US Dollar",
"to_currency": "NGN",
"to_currency_name": "Nigerian Naira"
}
]
},
"error": null
}
curl --request GET \
--url 'https://api.gravv.xyz/v1/transfer/supported-countries?iso_code=NG' \
--header 'Api-Key: <API_KEY>'
{
"data": {
"countries": [
{
"iso_code": "NG",
"name": "Nigeria",
"iso3_code": "NGA",
"currency_code": "NGN"
}
]
},
"error": null
}
Some countries have no currency in the list (currency_code null). For those, use the currency of the institution you pay. To see what is open, read Payments and supported countries.
Recipes
Each route has a step-by-step recipe with the requests, responses and errors. See Bank payouts and Remittance and crypto.