Pay a US bank account instantly by RTP

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

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

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"
}
}
'
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());
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())

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

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"
}
'
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());
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())
{
  "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 (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
Every error in the ACH recipe 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 accepts "rail": "rtp" to fund an account.

Reference

Was this page helpful?