Pay a supplier abroad by SWIFT
Look up the supplier's bank, save a SWIFT payee with an optional intermediary bank, upload the invoice and send a USD payout.
Use this recipe to look up a supplier's bank, save a SWIFT payee with an optional intermediary bank, upload the invoice and send a USD payout.
Who it is for. A US importer that pays a textile supplier in Lagos for an invoiced shipment.
Status. Available to Nigeria (NG) from bank-held USD accounts. Not yet available to other countries, or from wallet-backed accounts. SWIFT payouts are in USD only.
Money example
- Acme Imports' bank-held USD account holds 20,000.00 USD.
- Acme uploads invoice INV-7731 (a 180 KB PDF), then sends 12,000.00 USD to Adebayo Textiles Ltd's USD account at a Nigerian bank. In this example, the fee on its plan is 25.00 USD.
- Gravv debits the account 12,025.00 USD. Banks along the way, such as an intermediary bank or the receiving bank, may take their own charges out of the 12,000.00 USD. Gravv's fee is the
feein the response.
Before you start
- A bank-held USD account that is
activeand funded. Check it withGET /v2/accounts/{id}. For a country other than Nigeria, contact support before you build the flow, because no API lists the countries open for SWIFT. - The invoice or contract as a PDF, PNG or JPEG, at most 700 KB (in sandbox, under 500 KB).
- These details from the supplier:
- The legal name on the account.
- The account number, or the IBAN.
- The SWIFT/BIC code.
- The supplier's address.
- The bank's address.
- The intermediary (correspondent) bank's details, only if the supplier's bank gave you them.
Find out what is required
The bank and its address
Look the bank up by BIC. SWIFT banks appear only when you pass swift_code, search (3 characters or more) or institution_type=swift.
curl --request GET \
--url 'https://api.gravv.xyz/v1/external-accounts/institutions?swift_code=ZEIBNGLA' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/external-accounts/institutions?swift_code=ZEIBNGLA", {
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/institutions?swift_code=ZEIBNGLA",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
"data": {
"items": [
{
"account_number_type": "bank_account_number",
"address": "ZENITH HEIGHTS, PLOT 84 AJOSE ADEOGUN STREET, VICTORIA ISLAND, LAGOS",
"country_iso_code": "NG",
"currency": "USD",
"id": "fc7b9fc8-d85c-5473-9b62-f88b9eb5815b",
"max_amount": 0,
"min_amount": 0,
"name": "ZENITH BANK PLC",
"prefill": {
"bank_address_line_1": "87 AJOSE ADEOGUN STREET, ZENITH HEIGHTS",
"bank_city": "VICTORIA ISLAND",
"bank_country": "NG",
"bank_name": "ZENITH BANK PLC",
"bank_postal_code": "101241",
"bank_state": "LAGOS",
"bic": "ZEIBNGLA",
"institution_id": "fc7b9fc8-d85c-5473-9b62-f88b9eb5815b"
},
"swift_code": "ZEIBNGLA"
}
],
"items_per_page": 10,
"page": 1,
"total_items": 1,
"total_pages": 1
},
"error": null
}
Copy the fields in prefill into the payee. address is one display line. min_amount and max_amount are reference values, not limits. For a BIC that is not in the directory, the API returns HTTP 200 with an empty items list.
Address forms
GET /v1/transfer/supported-countries-for-address?iso_code=NG lists the country's states for address.state and bank_state. GET /v1/transfer/supported-countries lists the countries a payee may be in. A listed country does not mean payouts to it are open.
The SWIFT payee rules
- Set
payee_typetoswiftandcurrencytoUSD, the only currency allowed. - Send
bic, which must be in the SWIFT directory, andaccount_number. The account number is required even when you also sendiban. - Set
account_owner_typetobusinessfor a company orindividualfor a person. - Send the beneficiary's
addresswithaddress_line1,city,stateandcountry. - Send the bank's
bank_address_line_1,bank_city,bank_state,bank_postal_codeandbank_country. Gravv fillsbank_nameandinstitution_idfrom the BIC. - An intermediary bank is optional. If you add one, send all or none of
intermediary_financial_institution,intermediary_bank_routing_number,intermediary_bank_address1,intermediary_bank_city,intermediary_bank_state,intermediary_bank_zip_codeandintermediary_bank_country.intermediary_bank_address2is optional.
Whether a document is needed
Whether a SWIFT payout needs a document depends on a setting of your account at the bank, not on the payee. If one SWIFT payout from an account needed a document, expect all of them to need one. For business payments, always attach the invoice. If you send no document and one is needed, the transfer comes back paused. See Pay a person abroad by SWIFT.
Steps
1. Save the SWIFT 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: 53fe6ebb-1103-4731-915d-ea44ae648a0b' \
--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"
},
"intermediary_financial_institution": "Citibank N.A.",
"intermediary_bank_routing_number": "021000089",
"intermediary_bank_address1": "388 Greenwich Street",
"intermediary_bank_city": "New York",
"intermediary_bank_state": "NY",
"intermediary_bank_zip_code": "10013",
"intermediary_bank_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": "53fe6ebb-1103-4731-915d-ea44ae648a0b",
},
body: JSON.stringify({
"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"
},
"intermediary_financial_institution": "Citibank N.A.",
"intermediary_bank_routing_number": "021000089",
"intermediary_bank_address1": "388 Greenwich Street",
"intermediary_bank_city": "New York",
"intermediary_bank_state": "NY",
"intermediary_bank_zip_code": "10013",
"intermediary_bank_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": "53fe6ebb-1103-4731-915d-ea44ae648a0b",
},
json={
"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",
},
"intermediary_financial_institution": "Citibank N.A.",
"intermediary_bank_routing_number": "021000089",
"intermediary_bank_address1": "388 Greenwich Street",
"intermediary_bank_city": "New York",
"intermediary_bank_state": "NY",
"intermediary_bank_zip_code": "10013",
"intermediary_bank_country": "US",
},
)
print(response.status_code, response.json())
{
"data": {
"account_name": "ADEBAYO TEXTILES LTD",
"account_number": "1012345678",
"account_owner_type": "business",
"address": {
"address_line1": "12 Broad Street",
"city": "Lagos",
"country": "NG",
"postal_code": "102273",
"state": "Lagos"
},
"bic": "ZEIBNGLA",
"currency": "USD",
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"date_created": "2026-10-13T22:39:41.973606Z",
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"institution_country_iso_code": "NG",
"institution_id": "fc7b9fc8-d85c-5473-9b62-f88b9eb5815b",
"institution_name": "ZENITH BANK PLC",
"institution_type": "swift",
"payee_type": "swift",
"status": "pending"
},
"error": null
}
Gravv stores the bank and intermediary fields but does not return them.
2. Wait until the payee is active
A SWIFT payee can stay pending while the bank approves the receiving details, and there is no fixed time for this. Listen for external_account.active or external_account.setup_failed.
3. Upload the invoice
curl --request POST \
--url https://api.gravv.xyz/v1/transfer/documents \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 2251e6f0-754e-4c1b-a3b9-bb02e9afdf82' \
--data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"document_type": "invoice",
"description": "Invoice INV-7731 cotton fabric",
"file_name": "INV-7731.pdf",
"content_type": "application/pdf",
"content": "JVBERi0xLjQKJcfsj6IKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c+PgplbmRvYmoK"
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer/documents", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "2251e6f0-754e-4c1b-a3b9-bb02e9afdf82",
},
body: JSON.stringify({
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"document_type": "invoice",
"description": "Invoice INV-7731 cotton fabric",
"file_name": "INV-7731.pdf",
"content_type": "application/pdf",
"content": "JVBERi0xLjQKJcfsj6IKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c+PgplbmRvYmoK"
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/transfer/documents",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": "2251e6f0-754e-4c1b-a3b9-bb02e9afdf82",
},
json={
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"document_type": "invoice",
"description": "Invoice INV-7731 cotton fabric",
"file_name": "INV-7731.pdf",
"content_type": "application/pdf",
"content": "JVBERi0xLjQKJcfsj6IKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c+PgplbmRvYmoK",
},
)
print(response.status_code, response.json())
{
"data": {
"content_type": "application/pdf",
"created_at": "2026-10-13T22:39:57.665487Z",
"description": "Invoice INV-7731 cotton fabric",
"document_type": "invoice",
"file_name": "INV-7731.pdf",
"id": "5b0d8f3e-2a71-4c4f-9e0b-7d6c1a2b3c4d",
"size_bytes": 184233
},
"error": null
}
Store data.id. Upload a supporting document lists the document types and rules. You can attach up to 5 documents to a transfer. Each document works for one transfer only. Gravv deletes an unused document after 7 days.
4. Send the SWIFT payout
Gravv sends description to the bank as the purpose of payment, so say what the money is for. Set rail to swift. The wire rail is a US domestic wire, and the API refuses it for this payee.
curl --request POST \
--url https://api.gravv.xyz/v1/transfer \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: bd2f2a31-ea82-4680-a06c-9e9e949e849b' \
--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": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"rail": "swift"
},
"amount": "12000.00",
"description": "Payment for invoice INV-7731 cotton fabric",
"client_reference": "inv-7731",
"supporting_document_ids": [
"5b0d8f3e-2a71-4c4f-9e0b-7d6c1a2b3c4d"
]
}
'
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": "bd2f2a31-ea82-4680-a06c-9e9e949e849b",
},
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": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"rail": "swift"
},
"amount": "12000.00",
"description": "Payment for invoice INV-7731 cotton fabric",
"client_reference": "inv-7731",
"supporting_document_ids": [
"5b0d8f3e-2a71-4c4f-9e0b-7d6c1a2b3c4d"
]
}),
});
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": "bd2f2a31-ea82-4680-a06c-9e9e949e849b",
},
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": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"rail": "swift",
},
"amount": "12000.00",
"description": "Payment for invoice INV-7731 cotton fabric",
"client_reference": "inv-7731",
"supporting_document_ids": [
"5b0d8f3e-2a71-4c4f-9e0b-7d6c1a2b3c4d",
],
},
)
print(response.status_code, response.json())
{
"data": {
"amount": "12000",
"amount_currency": "USD",
"reference": "2d4f6a8c-0e1b-4d3f-a5c7-9e1b3d5f7a92",
"date_created": "2026-10-13T23:01:12.410Z",
"transfer_status": "pending",
"transaction_id": "6f8a0c2e-4b6d-4f8a-9c1e-3b5d7f9a1c03",
"local_amount": null,
"local_currency": null,
"fee": "25",
"fee_currency": "USD",
"requires_approval": false
},
"error": null
}
5. Follow it to the end
GET /v1/transactions/6f8a0c2e-4b6d-4f8a-9c1e-3b5d7f9a1c03 shows transaction_type global_remittance and channel wire. The webhooks carry workflow_type swift_transfer.
What can go wrong
| Response | Why | Fix |
|---|---|---|
400 bic ZEIBNGXX is not in the swift directory, so it cannot receive swift payouts: invalid input |
The BIC is unknown or mistyped | Look it up with institutions?swift_code= |
400 BIC must be 8 or 11 characters or BIC country code must be 2 letters |
The BIC has the wrong format | Use the 8- or 11-character BIC |
400 bic … is listed under …; name the bank_country or the institution_id: invalid input |
The BIC is listed in several countries | Send bank_country or institution_id |
400 required but missing for swift payee: account_number, bank_postal_code: invalid input |
Fields are missing. The message lists all of them at once | Add them. account_number is needed even with iban |
400 intermediary bank needs all of its fields; missing: intermediary_bank_zip_code: invalid input |
The intermediary bank fields are incomplete | Send all of the intermediary fields, or none |
400 EUR not supported for swift external accounts: invalid input |
SWIFT payees are USD only | To pay euros, use SEPA |
400 a minimum of $1 USD is required for SWIFT transfers |
The amount is below 1 USD | Send at least 1.00 USD |
400 payee type "swift" does not accept ACH/Wire transfers |
The request sent "rail": "wire" |
Use "rail": "swift" |
400 transfers from this account to DE/USD are not supported at this time |
Payouts to that country are not open for your account | Contact support |
200 pending, then transfer.status.failed with event_data.transfer_status manual_processing_required, transaction still pending |
Gravv accepted the transfer but holds it for a person to review, for example a SWIFT payout from a wallet-backed account. No money has left the account | Do not send it again. Contact support with the reference |
200 with transfer_status paused and status awaiting_supporting_document |
Your account needs a document and you sent none | Answer the pause |
400 supporting_document_ids: each id must be a document uploaded for this customer and not used by another transfer |
The document belongs to another customer, is already used, or is older than 7 days | Upload it again for this customer |
400 supporting_document_ids: at most 5 documents per transfer |
The request has too many ids, or lists one twice | Send up to 5 different ids |
400 content: the file is 812 KB (831488 bytes); the largest accepted is 700 KB (716800 bytes). … |
The file is over 700 KB | Compress it to 700 KB or less |
413 the uploaded file is too large — the maximum is 10MB |
In sandbox, a request of about 600 KB or more, whatever the message says. In live, a file over about 10 MB | Compress it to under 500 KB in sandbox, or to 700 KB or less in live |
400 content: the file is not a application/pdf |
content_type does not match the file |
Send the file's real type |
400 supporting documents cannot be checked right now; retry shortly |
The document store is briefly unavailable | Retry shortly with a new key. Gravv replays the 400 for 24 hours under the old key |
transfer.status.failed and a refund after the bank refused the payment |
The bank refused the payment for good, for example because the payee was saved against another account. Nothing was paid | Save the payee with internal_account_id set to the source, then send again |
Webhooks you will receive
You receive external_account.created, then external_account.active, then several transfer.status.pending events, then transfer.status.completed. If a document is missing, you receive transfer.requires_information.
Testing in sandbox
Sandbox simulates SWIFT payee setup. A business payee with an intermediary bank and an individual payee both become active within seconds. Uploads work as in live, except for size. Keep each file under 500 KB, because sandbox refuses requests from about 600 KB with a 413. The largest file that passed was 563 KB. You cannot force a document pause with a test value.