Pay a person abroad by SWIFT

Send a USD SWIFT payout to an individual, and answer a document pause by uploading a document and resuming the transfer.

Use this recipe to send a USD SWIFT payout to an individual. If the bank asks for a supporting document, the transfer pauses, and you upload the document and resume the transfer.

Who it is for. This recipe is for a US relocation company that pays an individual contractor's USD account in Abuja, where the bank asks for supporting documents.

Status. Available to Nigeria (NG) from bank-held USD accounts. Not yet available elsewhere.

Money example

  1. The company's bank-held USD account holds 9,000.00 USD.
  2. The company sends 4,800.00 USD to Chiamaka Eze with no document. In this example, the fee on its plan is 25.00 USD.
  3. The API returns HTTP 200 with transfer_status paused. Nothing is paid out yet.
  4. The company uploads the service contract and resumes the transfer. The payout goes ahead, and Gravv does not charge the fee again. If the company had done nothing for 72 hours, the transfer would have failed, and Gravv would have returned the amount and the fee.

Before you start

Find out what is required

The paused response tells you what to send. It carries requirements[].type supporting_document and expires_at. The transfer.requires_information webhook carries the same details.

A pause can also start later, shortly before the payout, if the bank's requirement changed. In that case you get only the webhook. Paused transfers explains every field of a pause.

Steps

1. Save the payee

Send the same request as for a supplier, but for a person and without an intermediary bank:

curl --request POST \
 --url https://api.gravv.xyz/v1/external-accounts \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: eab06884-22f2-4dd1-92a1-9289d8bd6a71' \
 --data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "swift",
"currency": "USD",
"account_name": "Chiamaka Eze",
"account_number": "2034567891",
"bic": "ZEIBNGLA",
"account_owner_type": "individual",
"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": "14 Aminu Kano Crescent",
"city": "Abuja",
"state": "FCT",
"postal_code": "900288",
"country": "NG"
}
}
'
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": "eab06884-22f2-4dd1-92a1-9289d8bd6a71",
},
body: JSON.stringify({
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"payee_type": "swift",
"currency": "USD",
"account_name": "Chiamaka Eze",
"account_number": "2034567891",
"bic": "ZEIBNGLA",
"account_owner_type": "individual",
"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": "14 Aminu Kano Crescent",
  "city": "Abuja",
  "state": "FCT",
  "postal_code": "900288",
  "country": "NG"
}
}),
});
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": "eab06884-22f2-4dd1-92a1-9289d8bd6a71",
},
json={
    "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "payee_type": "swift",
    "currency": "USD",
    "account_name": "Chiamaka Eze",
    "account_number": "2034567891",
    "bic": "ZEIBNGLA",
    "account_owner_type": "individual",
    "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": "14 Aminu Kano Crescent",
        "city": "Abuja",
        "state": "FCT",
        "postal_code": "900288",
        "country": "NG",
    },
},
)
print(response.status_code, response.json())

The response is HTTP 202 with status pending and the payee id, here a8098c1a-f86e-41c6-8b1e-4f2c3d7e9b10. Wait for the external_account.active webhook.

2. Send the payout

curl --request POST \
 --url https://api.gravv.xyz/v1/transfer \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: edf0bad7-f72f-4936-94fe-2bf05229ce54' \
 --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": "a8098c1a-f86e-41c6-8b1e-4f2c3d7e9b10",
"rail": "swift"
},
"amount": "4800.00",
"description": "Consulting services September 2026",
"client_reference": "contractor-cz-0926"
}
'
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": "edf0bad7-f72f-4936-94fe-2bf05229ce54",
},
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": "a8098c1a-f86e-41c6-8b1e-4f2c3d7e9b10",
  "rail": "swift"
},
"amount": "4800.00",
"description": "Consulting services September 2026",
"client_reference": "contractor-cz-0926"
}),
});
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": "edf0bad7-f72f-4936-94fe-2bf05229ce54",
},
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": "a8098c1a-f86e-41c6-8b1e-4f2c3d7e9b10",
        "rail": "swift",
    },
    "amount": "4800.00",
    "description": "Consulting services September 2026",
    "client_reference": "contractor-cz-0926",
},
)
print(response.status_code, response.json())
{
  "data": {
    "amount": "4800",
    "amount_currency": "USD",
    "reference": "c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607",
    "date_created": "2026-10-13T10:02:11.000Z",
    "transfer_status": "paused",
    "transaction_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
    "local_amount": null,
    "local_currency": null,
    "fee": "25",
    "fee_currency": "USD",
    "requires_approval": false,
    "status": "awaiting_supporting_document",
    "park_reason": "supporting_document",
    "pause_reason": "supporting_document_required",
    "requirements": [
      {
        "type": "supporting_document",
        "description": "resume with supporting_document_ids: at least one supporting document for this payout"
      }
    ],
    "expires_at": "2026-10-16T10:02:11Z"
  },
  "error": null
}

Store reference. It is the id you use to resume the transfer. GET /v1/transactions/0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b shows status paused. Do not tell your user the payment has gone.

3. Upload the document

Upload the document for the customer who sends the transfer. The file can be at most 700 KB. In sandbox, keep it under 500 KB.

curl --request POST \
 --url https://api.gravv.xyz/v1/transfer/documents \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: 8384f8ff-17ac-45b2-9f82-0ab2878b627f' \
 --data '
{
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"document_type": "contract",
"description": "Consulting contract September 2026",
"file_name": "contract-cz-2026.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": "8384f8ff-17ac-45b2-9f82-0ab2878b627f",
},
body: JSON.stringify({
"customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"document_type": "contract",
"description": "Consulting contract September 2026",
"file_name": "contract-cz-2026.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": "8384f8ff-17ac-45b2-9f82-0ab2878b627f",
},
json={
    "customer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "document_type": "contract",
    "description": "Consulting contract September 2026",
    "file_name": "contract-cz-2026.pdf",
    "content_type": "application/pdf",
    "content": "JVBERi0xLjQKJcfsj6IKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c+PgplbmRvYmoK",
},
)
print(response.status_code, response.json())

The response carries the document id, here 9f4c2e1a-7b3d-4c5e-8f6a-1b2c3d4e5f60.

4. Resume the transfer

A document pause accepts only supporting_document_ids. Use a new Idempotency-Key on every attempt.

curl --request POST \
 --url https://api.gravv.xyz/v1/transfer/c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607/resume \
 --header 'Api-Key: <API_KEY>' \
 --header 'Content-Type: application/json' \
 --header 'Idempotency-Key: 807402b0-ff02-44d3-81a0-a3bfc7c606f8' \
 --data '
{
"supporting_document_ids": [
"9f4c2e1a-7b3d-4c5e-8f6a-1b2c3d4e5f60"
]
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer/c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607/resume", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "807402b0-ff02-44d3-81a0-a3bfc7c606f8",
},
body: JSON.stringify({
"supporting_document_ids": [
  "9f4c2e1a-7b3d-4c5e-8f6a-1b2c3d4e5f60"
]
}),
});
console.log(response.status, await response.json());
import os

import requests

response = requests.post(
"https://api.gravv.xyz/v1/transfer/c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607/resume",
headers={
    "Api-Key": os.environ["GRAVV_API_KEY"],
    "Content-Type": "application/json",
    "Idempotency-Key": "807402b0-ff02-44d3-81a0-a3bfc7c606f8",
},
json={
    "supporting_document_ids": [
        "9f4c2e1a-7b3d-4c5e-8f6a-1b2c3d4e5f60",
    ],
},
)
print(response.status_code, response.json())
{
  "data": {
    "transfer_id": "c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607",
    "status": "approved_awaiting_execution",
    "transfer_status": "pending",
    "resumed": true
  },
  "error": null
}

resumed: true means the transfer goes ahead. Follow it as usual. status names an internal step, so do not build logic on it.

What can go wrong

Response Why Fix
400 this transfer is paused for a supporting document: resume it with supporting_document_ids only, without remitter_verification or additional_information The resume body has extra fields Send only supporting_document_ids
400 invalid request body: json: unknown field "documents" The resume endpoint rejects unknown fields, unlike the transfer endpoint Use the exact field name
422 ERROR_CODE_SUPPORTING_DOCUMENT_REQUIRED The resume sent an empty list Upload the document, then resume with its id
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 has expired Upload a new one for the sending customer
400 this transfer is not paused The transfer is already resumed, failed or completed Read the transaction
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
410 ERROR_CODE_TRANSFER_PAUSE_EXPIRED The pause passed expires_at. The transfer failed, and Gravv returned the fee Send a new transfer with the document attached
410 ERROR_CODE_TRANSFER_PAUSE_CANCELLED Gravv cancelled the pause Send a new transfer
404 transfer workflow not found The id is the transaction_id, or another business's transfer Resume with data.reference
409 the paused transfer changed while this request ran; please retry Two resume requests ran at the same time Retry once with a new key
400 insufficient balance on resume The balance was spent while the transfer was paused Fund the account, then resume with a new key
The same response as last time You reused the Idempotency-Key. Gravv replays the first response for 24 hours Use a new key for each attempt

Webhooks you will receive

  1. transfer.requires_information, with event_data.status paused, reason supporting_document_required and the requirements in limit_details.requirements.
  2. After you resume the transfer, transfer.resumed, then several transfer.status.pending events, then transfer.status.completed.
  3. If the pause ends without a resume, transfer.status.failed with reason supporting_document_expired (or supporting_document_cancelled).
{
  "tenant_id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
  "transaction_id": "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b",
  "event_category": "transfer",
  "event_type": "transfer.requires_information",
  "event_group_id": "c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607",
  "timestamp": "2026-10-13T10:02:11.402Z",
  "event_data": {
    "amount": "4800",
    "associated_transactions": [
      "0e1f2a3b-4c5d-4e6f-8a7b-9c0d1e2f3a4b"
    ],
    "client_reference": "contractor-cz-0926",
    "destination_id": "a8098c1a-f86e-41c6-8b1e-4f2c3d7e9b10",
    "limit_details": {
      "amount_usd": "4800.00",
      "reason": "supporting_document_required",
      "requirements": [
        {
          "description": "resume with supporting_document_ids: at least one supporting document for this payout",
          "type": "supporting_document"
        }
      ]
    },
    "reason": "supporting_document_required",
    "reference": "c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607",
    "status": "paused",
    "transfer_status": "awaiting_supporting_document",
    "workflow_type": "swift_transfer"
  },
  "event_id": "6b9d1c2e-3f4a-4b5c-8d6e-7f8091a2b3c4",
  "idempotency_key": "c1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607:2"
}

Testing in sandbox

You can test the upload and the resume checks (wrong ids, extra fields) in sandbox. A real document pause depends on the bank's setting for the account. You cannot force one with a test value.

Reference

Was this page helpful?