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. Store reference and transaction_id. reference is the transfer id. You use it to resume a paused transfer, and webhooks carry it as event_group_id. You use transaction_id to read the transaction.
  • paused: Gravv accepted the transfer, but it waits for you. Read requirements and see Paused transfers.
  • local_amount and local_currency are the amount priced for the recipient when there is a currency exchange. They are null when there is none.
  • fee is 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.

Was this page helpful?