Internal transfers
Move money between two accounts your customers hold at Gravv, without a payee.
An internal transfer (a book transfer) moves money between two accounts at Gravv, for example from a customer's operating account to their payroll account. You do not need a payee, and you do not choose a rail.
Example. The operating account holds 40,000.00 USD and the payroll account 1,000.00 USD. You move 15,000.00 USD. Afterward, the operating account holds 25,000.00 USD (less your fee, if your plan sets one) and the payroll account 16,000.00 USD.
Before you start
- Both accounts are
active. Check each one withGET /v2/accounts/{id}. - The source account holds the amount plus any fee.
- A bank-held account can only move money to another account held at the same bank. Compare
bank_codeon the two accounts. It cannot pay a card, a crypto wallet or an account at another bank. Wallet-backed accounts have no such limit.
Send the transfer
Send the source account, the destination account and the amount. Leave out rail.
curl --request POST \
--url https://api.gravv.xyz/v1/transfer \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 6f1a9c2e-4b7d-4e3a-9c5f-8d2b1a0e7c41' \
--data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"source": {
"source_type": "internal_account",
"id": "c56a4180-65aa-42ec-a945-5fd21dec0538"
},
"destination": {
"destination_type": "internal_account",
"id": "e4d909c2-90d0-4b6a-9c1d-3f2e5a7b8c90"
},
"amount": "15000.00",
"description": "Fund payroll October 15",
"client_reference": "book-2026-10-13-01"
}
'
{
"data": {
"amount": "15000",
"amount_currency": "USD",
"date_created": "2026-10-13T23:05:53.17901Z",
"fee": "0",
"fee_currency": "USD",
"local_amount": null,
"local_currency": null,
"reference": "1a7c795a-a3f2-4d45-ba57-2441b7c310b7",
"requires_approval": false,
"transaction_id": "94d7f1d0-9769-4762-a09c-ef8d172a8d1e",
"transfer_status": "pending"
},
"error": null
}
Gravv then completes the transfer in the background. The transaction has transaction_type internal_transfer and channel internal. Gravv sends the transfer.status.pending webhook, then transfer.status.completed.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 destination.id is required for an internal account transfer |
No destination id | Add destination.id |
400 the destination account was not found |
Wrong id, or an id from the other environment | Check the id. Sandbox ids do not exist in live |
400 invalid transfer input |
The destination is not active |
Wait until it is active |
400 this account can only send to a bank account or to another account held at the same bank |
A bank-held source paying an account at another bank, a card or a wallet | Pick a destination at the same bank |
400 amount must have at most 2 decimal places |
More than 2 decimals from a bank-held account | Send at most 2 decimals. A wallet-backed account accepts a third decimal at this step, but the transfer fails later |
400 insufficient balance |
Amount plus fee is more than the balance | Fund the source, then send with a new Idempotency-Key |
To move a customer's money to a card, see Fund cards. To move it on-chain, see Crypto transfers. The full recipe is Move money between your customer's own accounts.