Add an external account

Save a payee: a US bank account, a SWIFT, SEPA or UK account, a local bank account or a mobile wallet.

This page shows how to save a payee of each type. A payee (API name: external account) is the bank account or mobile wallet that you pay. Every transfer to someone outside Gravv pays a saved payee by its id, never raw bank details. Each payee belongs to one of your customers (customer_id).

Create one with POST /v1/external-accounts. Every request needs customer_id, currency, account_name, a new Idempotency-Key, and the fields of its type below.

Example. A payroll platform saves Maya Chen's US checking account for ACH, with her routing number, account number, address and payment_rail ach. The response is HTTP 202 with status pending. About a minute later, external_account.active arrives and the platform can pay her.

Payee types

payee_type Currency What it is Response
ach_wire USD A US bank account, for one rail: ach, wire or rtp 202 pending, then active
swift USD A bank abroad that receives USD 202 pending, then active
sepa EUR A euro IBAN in a SEPA country 202 pending, then active
fps GBP A UK sort code and account number 202 pending, then active
bank_account Local currency A local bank account, such as a Nigerian NUBAN 200 active
mobile_money Local currency A mobile wallet, identified by phone_number 200 active

Always send payee_type. Without it, Gravv guesses the type from the fields, and some guesses are wrong. For example, Gravv reads a GBP payee with a 9-digit routing number as a US account, and an 18-digit CLABE as a US account number.

Gravv sets up a pending payee with the bank in the background. Listen for external_account.active or external_account.setup_failed, or poll GET /v1/external-accounts/{external_account_id}. A transfer to a payee that is not active returns payee <id> is not active: pending.

US bank accounts (ACH, wire and RTP)

curl --request POST \
     --url https://api.gravv.xyz/v1/external-accounts \
     --header 'Api-Key: <API_KEY>' \
     --header 'Content-Type: application/json' \
     --header 'Idempotency-Key: 0d5a9e3c-6f2b-4c8d-b1e7-4a5b6c7d8ed9' \
     --data '
{
  "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "payee_type": "ach_wire",
  "currency": "USD",
  "account_name": "Maya Chen",
  "account_number": "000123456789",
  "routing_number": "021000021",
  "payment_rail": "ach",
  "bank_account_type": "checking",
  "account_owner_type": "individual",
  "bank_name": "JPMorgan Chase Bank",
  "internal_account_id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
  "address": {
    "address_line1": "350 Fifth Avenue",
    "city": "New York",
    "state": "NY",
    "postal_code": "10118",
    "country": "US"
  }
}
'
{
  "data": {
    "account_name": "MAYA CHEN",
    "account_number": "000123456789",
    "account_owner_type": "individual",
    "address": {
      "address_line1": "350 Fifth Avenue",
      "city": "New York",
      "country": "US",
      "postal_code": "10118",
      "state": "NY"
    },
    "currency": "USD",
    "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "date_created": "2026-10-13T22:38:41.310614Z",
    "id": "1b4e28ba-2fa1-41d2-883f-0016d3cca427",
    "institution_name": "JPMorgan Chase Bank",
    "payee_type": "ach_wire",
    "routing_number": "021000021",
    "status": "pending"
  },
  "error": null
}
  • payment_rail is required and has no default. Each payee is for one rail. To pay the same account by ACH and by wire, save two payees.
  • routing_number is the bank's 9-digit ABA number. For a wire, use the bank's wire routing number. Some banks have a different number for wires, and Gravv refuses a number that cannot receive wires. For rtp, the routing number must take instant payments.
  • bank_account_type is checking (the default) or savings. Gravv ignores a field called account_type.
  • account_owner_type is individual or business. bank_name and address are required. address.address_line1 is 4 to 34 characters, and the other address fields are at most 35. Gravv checks the postal code against address.country.
  • account_name may contain only unaccented Latin letters, spaces and ' - . , & / ( ). It must start with a letter and contain no digits.
  • If you pay from a bank-held account, set internal_account_id to that account, so Gravv registers the payee against it at the bank.
  • The response upper-cases account_name and returns your bank_name as institution_name. It does not return payment_rail, bank_account_type or internal_account_id, so store the rail on your side.

SWIFT payees (USD abroad)

Look the bank up first with GET /v1/external-accounts/institutions?swift_code=<BIC>. The response's prefill holds the bank fields, ready to copy.

curl --request POST \
 --url https://api.gravv.xyz/v1/external-accounts \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: 5e0b4f8d-1a3c-4d9e-82f6-5b6c7d8e9fea' \
 --data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "swift",
"currency": "USD",
"account_name": "Adebayo Textiles Ltd",
"account_number": "1012345678",
"bic": "ZEIBNGLA",
"account_owner_type": "business",
"internal_account_id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
"bank_address_line_1": "Plot 84 Ajose Adeogun Street",
"bank_city": "Lagos",
"bank_state": "Lagos",
"bank_postal_code": "101241",
"bank_country": "NG",
"address": {
"address_line1": "12 Broad Street",
"city": "Lagos",
"state": "Lagos",
"postal_code": "102273",
"country": "NG"
}
}
'
  • currency is USD only. For euros, use a sepa payee.
  • bic must be in the SWIFT directory (8 or 11 characters). If a BIC is listed in several countries, also send bank_country or institution_id.
  • account_number is required even when you also send iban.
  • The required bank fields are bank_address_line_1, bank_city, bank_state, bank_postal_code and bank_country. Gravv fills bank_name and institution_id from the BIC. One error lists all missing fields.
  • An intermediary (correspondent) bank is optional. If you send one, send all of intermediary_financial_institution, intermediary_bank_routing_number, intermediary_bank_address1, intermediary_bank_city, intermediary_bank_state, intermediary_bank_zip_code and intermediary_bank_country, or none of them. intermediary_bank_address2 is optional. Send an intermediary bank only if the beneficiary's bank gave you one.
  • The bank and intermediary fields are write-only. The response does not return them.
  • A SWIFT payee can stay pending while the bank approves its details.

SEPA payees (EUR)

curl --request POST \
 --url https://api.gravv.xyz/v1/external-accounts \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: 6f1c5a9e-2b4d-4eaf-93a7-6c7d8e9fa0fb' \
 --data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "sepa",
"currency": "EUR",
"account_name": "Lena Fischer",
"iban": "DE89370400440532013000",
"bic": "COBADEFFXXX",
"account_owner_type": "individual",
"bank_name": "Commerzbank",
"address": {
"address_line1": "Torstrasse 101",
"city": "Berlin",
"postal_code": "10119",
"country": "DE"
}
}
'

currency is EUR only, and iban is required. Gravv runs a mod-97 check on the IBAN. The IBAN's country must be a SEPA country, and Gravv takes the payee's country from it. bic is optional, and Gravv checks its format when you send it.

Faster Payments payees (GBP)

curl --request POST \
 --url https://api.gravv.xyz/v1/external-accounts \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: 7a2d6b0f-3c5e-4fb0-a4b8-7d8e9fa0b10c' \
 --data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "fps",
"currency": "GBP",
"account_name": "Oliver Bennett",
"routing_number": "601613",
"account_number": "31926819",
"account_owner_type": "individual",
"bank_name": "NatWest",
"address": {
"address_line1": "221 Baker Street",
"city": "London",
"postal_code": "NW1 6XE",
"country": "GB"
}
}
'

currency is GBP only. routing_number is the 6-digit sort code. account_number is 4 to 34 letters and digits with no spaces or dashes.

Local bank accounts

Pick the bank from GET /v1/external-accounts/institutions and send its id as institution_id. Gravv saves a local payee without an institution_id from that list, but you cannot pay it. The transfer returns this payee is not yet set up for transfers — please contact support to complete its setup.

curl --request POST \
 --url https://api.gravv.xyz/v1/external-accounts \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: 8b3e7c1a-4d6f-4ac1-b5c9-8e9fa0b1c21d' \
 --data '
{
"customer_id": "bc426080-4122-4cb1-8d46-ff4ffda26b44",
"payee_type": "bank_account",
"currency": "NGN",
"account_name": "Chinedu Eze",
"account_number": "0123456789",
"institution_id": "2ce313c8-de20-48dd-bfc6-949b8fd5ad84",
"institution_country_iso_code": "NG"
}
'

The response is HTTP 200 with status active. If the bank supports a name check, Gravv looks up the account when you save it and replaces account_name with the bank's name for the holder.

Account number rules by country:

Country Rule Error when it does not fit
Nigeria (NG) Exactly 10 digits (NUBAN) nigerian account numbers must be exactly 10 digits (NUBAN)
Ghana (GH) 10 to 16 digits ghanaian account numbers must be 10-16 digits
Kenya (KE) 8 to 20 letters or digits kenyan account numbers must be 8-20 alphanumeric characters
WAEMU: SN, CI, BJ, BF, GW, ML, NE, TG Exactly 24 letters or digits (the RIB) WAEMU account numbers must be exactly 24 alphanumeric characters
CEMAC: CM, CF, CG, GA, GQ, TD Exactly 23 digits CEMAC account numbers must be exactly 23 digits
Morocco (MA) Exactly 24 letters or digits (the RIB), and an institution_id for a Moroccan bank moroccan RIB numbers must be exactly 24 alphanumeric characters
Any other country 4 to 34 characters account numbers must be 4-34 characters

Gravv refuses spaces (account number must not contain spaces). Gravv checks payee rules when you save the payee, and decides whether it can pay out to the country when you send the transfer. See Payments and supported countries.

Some routes need more details on the payee. Mexico, Brazil, Colombia, India and Hong Kong are not yet available, but you can save payees with their details now:

Country Fields
Mexico payee_type bank_account, an 18-digit CLABE in account_number, national_id with national_id_type CURP or RFC
Brazil pix_key (1 to 77 characters, no spaces), national_id with national_id_type CPF or CNPJ
Colombia account_number, institution_id, bank_account_type, national_id with national_id_type CC or NIT
India account_number (9 to 18 digits), the IFSC in routing_number
Hong Kong account_number, institution_id
All of them address with address_line1, city and country

Send national_id and national_id_type together. The response returns national_id_type and a masked national_id_masked. Gravv never returns the full id or the PIX key.

Mobile wallets

curl --request POST \
 --url https://api.gravv.xyz/v1/external-accounts \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: 9c4f8d2b-5e7a-4bd2-86da-9fa0b1c2d32e' \
 --data '
{
"customer_id": "e0c40bf4-c1a6-4714-bde1-316a1c15e956",
"payee_type": "mobile_money",
"currency": "RWF",
"account_name": "Claudine Uwase",
"phone_number": "+250788123456",
"institution_id": "e3077661-0359-4740-b6bb-bad5f4e965fd"
}
'
  • Put the number in phone_number, in international form with +. For a mobile wallet, Gravv refuses a number in account_number.
  • Pick the operator from the institutions list with institution_type=mobile_money. Its account_number_type is phone_number. Some countries list the same operator twice. Pick the row that the recipe for that country names.
  • In some countries (Kenya, and the operators listed as MTN MoMo <country>), Gravv looks up the wallet's registered name when you save the payee and replaces account_name with it. Gravv refuses a number that the operator cannot find at once, with account verification failed: bank could not resolve account holder: invalid input.
  • Gravv does not check or reformat other numbers when you save them. Gravv refuses a number in the wrong format only when you pay, so check numbers yourself. You cannot change phone_number later, so a wrong number needs a new payee.

Duplicates

Gravv refuses a payee that matches one the customer already has. It returns HTTP 400, with the existing id in data.payee_id:

{
"data": {
"payee_id": "1b4e28ba-2fa1-41d2-883f-0016d3cca427"
},
"error": "a recipient with the same account name, account number and bank already exists"
}
  • A payee matches if it has the same customer, account name, account number and bank. For US payees, the routing number and the rail are part of the match, so an ACH and a wire payee for one account are two payees. For Faster Payments, Gravv compares the sort code digits (60-16-13 equals 601613).
  • Deleted and setup_failed payees still count. Fix a failed one with Update an external account instead of saving it again.
  • When the bank's name check replaces account_name, the duplicate check can miss, and Gravv saves a second payee. Look for an existing payee before you create one, with GET /v1/external-accounts?customer_id=<id>&search=<account number>.

Statuses and events

Status Meaning
pending Gravv is setting up the payee with the bank. Gravv retries setup a limited number of times, then the payee becomes setup_failed
active You can pay the payee
setup_failed Setup failed. error_message says why. Correct the payee with PATCH
archived The payee is no longer in use
deleted The payee is deleted. It still counts as a duplicate

Gravv sends external_account.created, then external_account.active or external_account.setup_failed. event_group_id is the payee id. event_data is the full payee, including the full account_number, so protect your webhook logs.

Errors

Payee errors are HTTP 400, with the reason in error. The reason sometimes ends in : invalid input, so match on the start of the message. Some errors also carry a code. After any 4xx, fix the request and send it with a new Idempotency-Key. For 24 hours, the old key replays the old response.

Error Fix
payment rail required but missing for ach/wire payee (expected ach, wire, or rtp): invalid input Add payment_rail
routing number checksum invalid: invalid input / routing number must be exactly 9 digits: invalid input Use the bank's real ABA number
routing number <number> cannot receive wires; use the bank's wire routing number: invalid input Ask for the wire routing number, or save the payee for ACH
routing number <number> cannot receive instant payments; use ach or wire: invalid input Save the payee for ACH or wire
account name may contain only unaccented Latin letters (A-Z); "é" is not allowed: invalid input Transliterate the name
bic <BIC> is not in the swift directory, so it cannot receive swift payouts: invalid input Look the BIC up with institutions?swift_code=
required but missing for swift payee: account_number, bank_postal_code: invalid input Add every listed field
intermediary bank needs all of its fields; missing: intermediary_bank_zip_code: invalid input Send the whole intermediary block, or none
EUR not supported for swift external accounts: invalid input SWIFT is USD only. Use SEPA for EUR
IBAN checksum invalid (mod 97 = 28, expected 1): invalid input Check the IBAN
routing number (sort code) required but missing for fps payee: invalid input Send the sort code in routing_number
phone number is required for mobile money payees: invalid input Put the number in phone_number
account verification failed: bank could not resolve account holder: invalid input Check the number with the recipient
account verification temporarily unavailable; please retry: invalid input Retry later with a new key
national id type CPF is not accepted for MX payees (expected CURP, RFC) Use an id type for that country
CLABE checksum invalid (expected check digit X, got Y): invalid input Re-enter the 18 digits
Was this page helpful?