---
title: "Move money between own accounts"
description: "Move money between two accounts your customer holds at Gravv with an internal transfer, which needs no payee and no rail."
---
Use this recipe to move money between two accounts your customer holds at Gravv with an internal transfer.

**Who it is for.** A treasury app that moves a customer's operating cash into their payroll account before payday.

**Status.** Available. A bank-held account can move money only to another account at the same bank.

## Money example

1. The operating account holds 40,000.00 USD and the payroll account holds 1,000.00 USD.
2. Move 15,000.00 USD from operating to payroll.
3. The operating account now holds 25,000.00 USD, less your fee if your plan has one. The payroll account holds 16,000.00 USD.

## Before you start

Both accounts must be `active`. Read each one with [`GET /v2/accounts/{id}`](/api-reference/accounts-v2/get-v2-accounts-id). If the source is bank-held, both accounts must show the same `bank_code`.

## Find out what is required

You need only the two account ids. An internal transfer has no payee and no `rail`.

## Steps

### 1. Send the internal transfer

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/transfer \
     --header 'Api-Key: <API_KEY>' \
     --header 'Content-Type: application/json' \
     --header 'Idempotency-Key: cae850ff-8e57-4c0c-bb78-1788523e38c2' \
     --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"
}
'
```
```javascript title="Node"
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": "cae850ff-8e57-4c0c-bb78-1788523e38c2",
  },
  body: JSON.stringify({
    "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"
  }),
});
console.log(response.status, await response.json());
```
```python title="Python"
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": "cae850ff-8e57-4c0c-bb78-1788523e38c2",
    },
    json={
        "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",
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "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
}
```

### 2. Follow it to the end

[`GET /v1/transactions/94d7f1d0-9769-4762-a09c-ef8d172a8d1e`](/api-reference/transactions/get-v1-transactions-transaction-id) shows `transaction_type` `internal_transfer` and `channel` `internal`, with `balance_before` and `balance_after` on the debit. The webhooks carry `workflow_type` `internal_transfer`.

## What can go wrong

| Response | Why | Fix |
| :-- | :-- | :-- |
| 400 `destination.id is required for an internal account transfer` | The request has no destination id | Add `destination.id` |
| 400 `the destination account was not found` | A wrong id, or one 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` | The source is bank-held and the destination is 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 | Send `"15000.00"`. Always round on your side. From a wallet-backed account, the API can accept an amount with more decimals, and the transfer then fails |
| 400 `insufficient balance` | The amount plus the fee is more than the balance | Fund the source first |

## Webhooks you will receive

You receive `transfer.status.pending` (twice in sandbox), then `transfer.status.completed`. The first `pending` event has no top-level `transaction_id`. Later events carry it, with the debit and credit rows in `event_data.associated_transactions`.

## Testing in sandbox

Fund the source with [`POST /v2/accounts/simulate/deposit`](/api-reference/accounts-v2/post-v2-accounts-simulate-deposit), then send the internal transfer. In sandbox, a move between two NGN accounts completed about 90 seconds after the request.

## Reference

- [Internal transfers](/platform/transfers/internal-transfers)
- [Create a transfer](/api-reference/transfer/post-v1-transfer)
