---
title: "Pay a US bank account instantly by RTP"
description: "Save an RTP payee and send an instant USD payment from a bank-held USD account."
---
Use this recipe to save an RTP payee and send an instant USD payment from a bank-held USD account.

**Who it is for.** An on-demand staffing app that pays a shift worker the same minute it approves the shift.

**Status.** Available from bank-held USD accounts, if the receiving bank takes instant payments. Not supported from wallet-backed accounts.

## Money example

1. The staffing app's bank-held USD account holds 3,000.00 USD.
2. The app pays 180.00 USD to Luis Ortega by RTP. In this example, the fee on its plan is 0.50 USD.
3. Gravv debits the account 180.50 USD. Luis's bank credits 180.00 USD within seconds.

## Before you start

- Everything in [Pay a US bank account by ACH](/recipes/bank-payouts/pay-a-us-bank-account-by-ach#before-you-start).
- **The source must be a bank-held USD account.** A wallet-backed account cannot pay by RTP.

## Find out what is required

Save the payee with `"payment_rail": "rtp"`. When you save the payee, Gravv checks the routing number against the instant-payments list. If the bank is not on the list, the API returns a 400 that tells you to use ACH or wire.

## Steps

### 1. Save the RTP payee

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/external-accounts \
     --header 'Api-Key: <API_KEY>' \
     --header 'Content-Type: application/json' \
     --header 'Idempotency-Key: 81981a8a-21d2-4920-bdb4-5633cba433cd' \
     --data '
{
  "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "payee_type": "ach_wire",
  "currency": "USD",
  "account_name": "Luis Ortega",
  "account_number": "7781203345",
  "routing_number": "021000021",
  "payment_rail": "rtp",
  "bank_account_type": "checking",
  "account_owner_type": "individual",
  "bank_name": "JPMorgan Chase Bank",
  "internal_account_id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
  "address": {
    "address_line1": "1450 Oak Street",
    "city": "Chicago",
    "state": "IL",
    "postal_code": "60610",
    "country": "US"
  }
}
'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/external-accounts", {
  method: "POST",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": "81981a8a-21d2-4920-bdb4-5633cba433cd",
  },
  body: JSON.stringify({
    "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "payee_type": "ach_wire",
    "currency": "USD",
    "account_name": "Luis Ortega",
    "account_number": "7781203345",
    "routing_number": "021000021",
    "payment_rail": "rtp",
    "bank_account_type": "checking",
    "account_owner_type": "individual",
    "bank_name": "JPMorgan Chase Bank",
    "internal_account_id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
    "address": {
      "address_line1": "1450 Oak Street",
      "city": "Chicago",
      "state": "IL",
      "postal_code": "60610",
      "country": "US"
    }
  }),
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.post(
    "https://api.gravv.xyz/v1/external-accounts",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
        "Content-Type": "application/json",
        "Idempotency-Key": "81981a8a-21d2-4920-bdb4-5633cba433cd",
    },
    json={
        "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "payee_type": "ach_wire",
        "currency": "USD",
        "account_name": "Luis Ortega",
        "account_number": "7781203345",
        "routing_number": "021000021",
        "payment_rail": "rtp",
        "bank_account_type": "checking",
        "account_owner_type": "individual",
        "bank_name": "JPMorgan Chase Bank",
        "internal_account_id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
        "address": {
            "address_line1": "1450 Oak Street",
            "city": "Chicago",
            "state": "IL",
            "postal_code": "60610",
            "country": "US",
        },
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

The response is HTTP 202 with `status` `pending` and the payee `id`, here `3d813cbb-47fb-42ba-91df-831e1593ac29`. Wait for the `external_account.active` webhook.

### 2. Send the payment

<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: e6ecc4de-197e-45e9-9a97-7140e2a3338e' \
     --data '
{
  "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "source": {
    "source_type": "internal_account",
    "id": "c56a4180-65aa-42ec-a945-5fd21dec0538"
  },
  "destination": {
    "destination_type": "external_account",
    "id": "3d813cbb-47fb-42ba-91df-831e1593ac29",
    "rail": "rtp"
  },
  "amount": "180.00",
  "description": "Shift 88123 pay",
  "client_reference": "shift-88123"
}
'
```
```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": "e6ecc4de-197e-45e9-9a97-7140e2a3338e",
  },
  body: JSON.stringify({
    "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "source": {
      "source_type": "internal_account",
      "id": "c56a4180-65aa-42ec-a945-5fd21dec0538"
    },
    "destination": {
      "destination_type": "external_account",
      "id": "3d813cbb-47fb-42ba-91df-831e1593ac29",
      "rail": "rtp"
    },
    "amount": "180.00",
    "description": "Shift 88123 pay",
    "client_reference": "shift-88123"
  }),
});
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": "e6ecc4de-197e-45e9-9a97-7140e2a3338e",
    },
    json={
        "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "source": {
            "source_type": "internal_account",
            "id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
        },
        "destination": {
            "destination_type": "external_account",
            "id": "3d813cbb-47fb-42ba-91df-831e1593ac29",
            "rail": "rtp",
        },
        "amount": "180.00",
        "description": "Shift 88123 pay",
        "client_reference": "shift-88123",
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "amount": "180",
    "amount_currency": "USD",
    "reference": "9c2e4a6b-8d0f-4b2c-9e4a-6c8e0a2b4d34",
    "date_created": "2026-10-13T16:45:10.311Z",
    "transfer_status": "pending",
    "transaction_id": "4b6d8f0a-2c4e-4d6f-8a0c-4e6a8c0e2f45",
    "local_amount": null,
    "local_currency": null,
    "fee": "0.5",
    "fee_currency": "USD",
    "requires_approval": false
  },
  "error": null
}
```

RTP has no note for the receiving bank. The API accepts `payment_rail_reference` but does not send it.

### 3. Follow it to the end

Read [`GET /v1/transactions/4b6d8f0a-2c4e-4d6f-8a0c-4e6a8c0e2f45`](/api-reference/transactions/get-v1-transactions-transaction-id) (it shows `channel` `rtp`), or wait for `transfer.status.completed`.

## What can go wrong

| Response | Why | Fix |
| :-- | :-- | :-- |
| 400 `routing number 021000021 cannot receive instant payments; use ach or wire: invalid input` | The receiving bank is not on the instant-payments network | Save the payee for `ach` or `wire` instead, with a new key |
| 400 `transfers to US/USD are not supported at this time` | The source is wallet-backed. RTP pays only from bank-held USD accounts | Use ACH or wire, or pay from a bank-held account |
| 400 `transfers from this account to US/USD are not supported at this time` | The payee was registered against another bank-held account | Save the RTP payee with `internal_account_id` set to the source |
| 400 `this payee is registered for ach transfers, not rtp — …` | You saved the payee for ACH | Save an RTP payee. See [two rails](/recipes/bank-payouts/save-one-account-for-two-rails) |
| Every error in [the ACH recipe](/recipes/bank-payouts/pay-a-us-bank-account-by-ach#what-can-go-wrong) also applies to RTP | | |

## Webhooks you will receive

You receive the same webhooks as for ACH: `external_account.created`, then `external_account.active`, then `transfer.status.pending`, then `transfer.status.completed`.

## Testing in sandbox

Sandbox refuses every routing number tried for RTP so far. `021000021`, `026009593`, `121000248` and `011000028` all return `cannot receive instant payments; use ach or wire`, so you cannot save an RTP payee in sandbox yet. You can still test the refusal and your fallback to ACH or wire. [`POST /v2/accounts/simulate/deposit`](/api-reference/accounts-v2/post-v2-accounts-simulate-deposit) accepts `"rail": "rtp"` to fund an account.

## Reference

- [Add an external account](/api-reference/external-accounts/post-v1-external-accounts)
- [Create a transfer](/api-reference/transfer/post-v1-transfer)
- [Get a transaction](/api-reference/transactions/get-v1-transactions-transaction-id)
