Save one US account for both ACH and wire
Save two payees for the same US account, one per rail, and pay each on its own rail.
Use this recipe to save two payees for the same US account, one for each rail, and pay each payee on its own rail.
Who it is for. An accounts-payable app that pays a vendor by ACH for routine invoices and by wire for urgent ones.
Status. Available.
Money example
- The vendor is Northwind Supply, with account number 000555777999 and routing number 021000021.
- The app pays a routine invoice of 1,200.00 USD by ACH to payee A.
- The app pays an urgent invoice of 25,000.00 USD by wire to payee B, which is the same account.
Before you start
You need everything in Pay a US bank account by ACH.
Find out what is required
For US payees, a duplicate has the same customer, account name, account number, routing number and rail. A different rail makes a different payee. First, list the payees that already exist:
curl --request GET \
--url 'https://api.gravv.xyz/v1/external-accounts?customer_id=7c9e6679-7425-40de-944b-e07fc1f90ae7&search=000555777999' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/external-accounts?customer_id=7c9e6679-7425-40de-944b-e07fc1f90ae7&search=000555777999", {
method: "GET",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os
import requests
response = requests.get(
"https://api.gravv.xyz/v1/external-accounts?customer_id=7c9e6679-7425-40de-944b-e07fc1f90ae7&search=000555777999",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
The list has no status filter, so check status on each item. The API does not return a payee's rail, so match the ids against your own records.
Steps
1. Save payee A for ACH
curl --request POST \
--url https://api.gravv.xyz/v1/external-accounts \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 11dada0c-ecc7-436f-9f1a-d46bc7eb86c3' \
--data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "ach_wire",
"currency": "USD",
"account_name": "Northwind Supply Inc",
"account_number": "000555777999",
"routing_number": "021000021",
"payment_rail": "ach",
"account_owner_type": "business",
"bank_name": "JPMorgan Chase Bank",
"address": {
"address_line1": "80 Pine Street",
"city": "New York",
"state": "NY",
"postal_code": "10005",
"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": "11dada0c-ecc7-436f-9f1a-d46bc7eb86c3",
},
body: JSON.stringify({
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "ach_wire",
"currency": "USD",
"account_name": "Northwind Supply Inc",
"account_number": "000555777999",
"routing_number": "021000021",
"payment_rail": "ach",
"account_owner_type": "business",
"bank_name": "JPMorgan Chase Bank",
"address": {
"address_line1": "80 Pine Street",
"city": "New York",
"state": "NY",
"postal_code": "10005",
"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": "11dada0c-ecc7-436f-9f1a-d46bc7eb86c3",
},
json={
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "ach_wire",
"currency": "USD",
"account_name": "Northwind Supply Inc",
"account_number": "000555777999",
"routing_number": "021000021",
"payment_rail": "ach",
"account_owner_type": "business",
"bank_name": "JPMorgan Chase Bank",
"address": {
"address_line1": "80 Pine Street",
"city": "New York",
"state": "NY",
"postal_code": "10005",
"country": "US",
},
},
)
print(response.status_code, response.json())
The response is HTTP 202 with status pending. Store the id with its rail, for example {"payee_id": "a1f2…", "rail": "ach"}.
2. Save payee B for wire
Send the same body with "payment_rail": "wire" and a new Idempotency-Key:
curl --request POST \
--url https://api.gravv.xyz/v1/external-accounts \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 762adba6-ce4b-4b4e-99d9-b5effc3e9336' \
--data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "ach_wire",
"currency": "USD",
"account_name": "Northwind Supply Inc",
"account_number": "000555777999",
"routing_number": "021000021",
"payment_rail": "wire",
"account_owner_type": "business",
"bank_name": "JPMorgan Chase Bank",
"address": {
"address_line1": "80 Pine Street",
"city": "New York",
"state": "NY",
"postal_code": "10005",
"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": "762adba6-ce4b-4b4e-99d9-b5effc3e9336",
},
body: JSON.stringify({
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "ach_wire",
"currency": "USD",
"account_name": "Northwind Supply Inc",
"account_number": "000555777999",
"routing_number": "021000021",
"payment_rail": "wire",
"account_owner_type": "business",
"bank_name": "JPMorgan Chase Bank",
"address": {
"address_line1": "80 Pine Street",
"city": "New York",
"state": "NY",
"postal_code": "10005",
"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": "762adba6-ce4b-4b4e-99d9-b5effc3e9336",
},
json={
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "ach_wire",
"currency": "USD",
"account_name": "Northwind Supply Inc",
"account_number": "000555777999",
"routing_number": "021000021",
"payment_rail": "wire",
"account_owner_type": "business",
"bank_name": "JPMorgan Chase Bank",
"address": {
"address_line1": "80 Pine Street",
"city": "New York",
"state": "NY",
"postal_code": "10005",
"country": "US",
},
},
)
print(response.status_code, response.json())
3. Pay each payee on its own rail
Pay payee A with "rail": "ach" and payee B with "rail": "wire" inside destination, as in the ACH recipe and the wire recipe.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 a recipient with the same account name, account number and bank already exists, with data.payee_id |
You sent the same rail twice, or an earlier payee on this rail exists, including deleted and setup_failed payees |
Use data.payee_id. If it is setup_failed, fix it |
422 idempotency_key_reused on step 2 |
You reused the key from step 1 with a changed body | Use a new key for each payee |
400 this payee is registered for ach transfers, not wire — initiate on rail "ach", or add a payee on the wire rail |
You paid payee A on the wire rail | Pay payee B instead, or save payee B first |
| The API accepts two payees for the same vendor with different name spellings | The name is part of the duplicate check | Normalize names on your side |
Webhooks you will receive
You receive external_account.created and external_account.active for each payee, then the transfer events as in the ACH recipe.
Testing in sandbox
Save both payees in sandbox. The duplicate check works the same way as in live, and sandbox simulates payee setup.