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
- The company's bank-held USD account holds 9,000.00 USD.
- 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.
- The API returns HTTP 200 with
transfer_statuspaused. Nothing is paid out yet. - 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
- Everything in Pay a supplier abroad by SWIFT.
- The person's home address. On the payee, set
account_owner_typetoindividual.
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
transfer.requires_information, withevent_data.statuspaused,reasonsupporting_document_requiredand the requirements inlimit_details.requirements.- After you resume the transfer,
transfer.resumed, then severaltransfer.status.pendingevents, thentransfer.status.completed. - If the pause ends without a resume,
transfer.status.failedwithreasonsupporting_document_expired(orsupporting_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.