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_railis 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_numberis 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. Forrtp, the routing number must take instant payments.bank_account_typeischecking(the default) orsavings. Gravv ignores a field calledaccount_type.account_owner_typeisindividualorbusiness.bank_nameandaddressare required.address.address_line1is 4 to 34 characters, and the other address fields are at most 35. Gravv checks the postal code againstaddress.country.account_namemay 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_idto that account, so Gravv registers the payee against it at the bank. - The response upper-cases
account_nameand returns yourbank_nameasinstitution_name. It does not returnpayment_rail,bank_account_typeorinternal_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"
}
}
'
currencyisUSDonly. For euros, use asepapayee.bicmust be in the SWIFT directory (8 or 11 characters). If a BIC is listed in several countries, also sendbank_countryorinstitution_id.account_numberis required even when you also sendiban.- The required bank fields are
bank_address_line_1,bank_city,bank_state,bank_postal_codeandbank_country. Gravv fillsbank_nameandinstitution_idfrom 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_codeandintermediary_bank_country, or none of them.intermediary_bank_address2is 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
pendingwhile 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 inaccount_number. - Pick the operator from the institutions list with
institution_type=mobile_money. Itsaccount_number_typeisphone_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 replacesaccount_namewith it. Gravv refuses a number that the operator cannot find at once, withaccount 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_numberlater, 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-13equals601613). - Deleted and
setup_failedpayees 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, withGET /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 |