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

  1. Acme Imports' bank-held USD account holds 20,000.00 USD.
  2. 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.
  3. 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 fee in the response.

Before you start

  • A bank-held USD account that is active and funded. Check it with GET /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_type to swift and currency to USD, the only currency allowed.
  • Send bic, which must be in the SWIFT directory, and account_number. The account number is required even when you also send iban.
  • Set account_owner_type to business for a company or individual for a person.
  • Send the beneficiary's address with address_line1, city, state and country.
  • Send the bank's bank_address_line_1, bank_city, bank_state, bank_postal_code and bank_country. Gravv fills bank_name and institution_id from 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_code and intermediary_bank_country. intermediary_bank_address2 is 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.

Reference

Was this page helpful?