Tell why a transfer is stuck
Tell a transfer that is still moving from one that is paused, under review or failed, and take the one action that applies without sending the money twice.
Use this recipe to tell a transfer that is still moving from one that is paused, under review or failed. You then take the one action that applies, and you do not send the money twice. It is for a developer at a remittance app whose user says "my 300 USD to Ghana has said processing for two days".
Status. Available.
Money example
Kofi's 300 USD payout can be in one of four states, and each needs a different answer.
- Gravv is still waiting on the bank or the mobile-money operator. Wait.
- It is paused because sender details are missing. Send the details and resume it, and the transfer continues.
- It is under review. The money may already have left, so do not send it again. Contact support.
- It failed, and 300 USD comes back as a refund row. Fix the cause and send a new transfer.
Before you start
You have the transaction_id and the reference from POST /v1/transfer.
Find out what is required
Read the transaction row first, then the latest webhook.
Row status |
remark or webhook |
Meaning | What to do |
|---|---|---|---|
pending |
No remark. transfer.status.pending events keep arriving |
Moving. Gravv is waiting on a bank, an operator or the chain. A transfer does not fail because of time alone | Wait |
paused |
transfer.requires_information, with reason and limit_details.requirements[] |
Waiting on you for a limit, screening, sender details or a document | Send what requirements lists to POST /v1/transfer/{reference}/resume before expires_at |
waiting_approval |
None | Created in the Dashboard and waiting for an approver. Transfers sent with an API key never wait for approval | Approve it in the Dashboard |
pending |
Transfer is under review. Please contact support before trying again. The webhook may say transfer.status.failed |
Under review. The money may already have left | Do not send again. Contact support with the reference |
failed |
A remark such as The recipient's account details are invalid. Your transfer has been refunded., and a transfer_refund row |
Failed and refunded | Fix the cause, then send a new transfer with a new client_reference |
failed |
transfer.status.failed with reason limit_pause_expired, supporting_document_expired or sender_details_expired (or _cancelled) |
A pause ran out or was cancelled | Send a new transfer |
completed |
Then transfer.status.returned |
Paid, then returned by the bank. A new incoming row appears | Check the payee's details before sending again |
Steps
1. Read the row
curl --request GET \
--url https://api.gravv.xyz/v1/transactions/29a3b2e9-5d65-4441-9588-42dea2bc372f \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/transactions/29a3b2e9-5d65-4441-9588-42dea2bc372f", {
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/transactions/29a3b2e9-5d65-4441-9588-42dea2bc372f",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
"data": {
"id": "29a3b2e9-5d65-4441-9588-42dea2bc372f",
"amount": "300",
"date_created": "2026-10-11T09:12:03.7Z",
"direction": "out",
"transaction_type": "global_remittance",
"reference": "7412b293-4729-4739-a14f-f3d719db3ad0",
"channel": "mobile_money",
"status": "pending",
"remark": "Transfer is under review. Please contact support before trying again.",
"client_reference": "kofi-gh-0042",
"currency": "USD"
},
"error": null
}
Here the transfer is under review. Stop, and contact support with the reference 7412b293-4729-4739-a14f-f3d719db3ad0.
2. Read the latest event
List the transfer's events, then open the newest with GET /v1/webhooks/event/{id} and read payload.event_type, payload.event_data.reason and payload.event_data.limit_details.
curl --request GET \
--url 'https://api.gravv.xyz/v1/webhooks/history?transaction_id=29a3b2e9-5d65-4441-9588-42dea2bc372f&event_type=transfer' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/webhooks/history?transaction_id=29a3b2e9-5d65-4441-9588-42dea2bc372f&event_type=transfer", {
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/webhooks/history?transaction_id=29a3b2e9-5d65-4441-9588-42dea2bc372f&event_type=transfer",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
3. If it is paused, answer the pause
Use Check what a sender can still send to see how much the sender has left. Then resume the transfer with only the fields requirements asks for and a new Idempotency-Key. Get a stuck remittance moving shows each kind of pause and its resume body.
4. If it failed, confirm the refund
List the customer's transfer_refund rows.
curl --request GET \
--url 'https://api.gravv.xyz/v1/transactions?customer_id=5be6128e-18c2-4797-a142-ea7d17be3111&transaction_type=transfer_refund' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/transactions?customer_id=5be6128e-18c2-4797-a142-ea7d17be3111&transaction_type=transfer_refund", {
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/transactions?customer_id=5be6128e-18c2-4797-a142-ea7d17be3111&transaction_type=transfer_refund",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
Match the refund row to the transfer by reference.
What can go wrong
| Response | Why | Fix |
|---|---|---|
The webhook says transfer.status.failed, but the row is pending |
Gravv sends transfer.status.failed for a transfer under review |
Trust the row. The transfer is under review, not failed. Do not send it again |
The webhook says transfer.status.completed, but the row is still pending |
The row's last update was delayed | Check again later. If it stays pending, report the reference to support |
410 ERROR_CODE_TRANSFER_PAUSE_EXPIRED on a resume |
The resume came after expires_at |
Send a new transfer |
200 with "resumed": false and new requirements |
The transfer is still over the limit after your answer | Supply what the new requirements lists |
| A resume replays an earlier answer | You reused an earlier attempt's Idempotency-Key |
Use a new key for every attempt |
404 transfer workflow not found on a resume |
You sent the transaction_id |
Use the reference |
400 this transfer is not paused |
The transfer is moving, under review or final | Use the table above |
POST /v1/transfer returned 200 and you treated the transfer as sent |
A 200 can carry transfer_status paused |
Read transfer_status on every response |
| A payout went out twice after a timeout | You retried the transfer with a new Idempotency-Key, or without a client_reference |
Retry a timeout with the same key and the same client_reference |
Webhooks you will receive
transfer.status.pending, many times while the transfer moves.transfer.requires_informationfor a pause, withlimit_details.transfer.resumed, withreasonresumedorreleased.transfer.status.completed.transfer.status.failed, with areasonwhen a pause ended.transfer.status.returned.
Testing in sandbox
A transfer above your sandbox limits policy produces a pause when the policy is in enforce mode. You cannot force a failed crypto transfer or a review from the API. Both are Not yet available in sandbox.
For example, a 5 USD sandbox mobile-money payout went to review. Its webhook was transfer.status.failed with no reason, and the row stayed pending with remark null. In sandbox, a pending row whose latest webhook says failed is the under-review case, even without the remark.