Get a stuck remittance moving
Tell which of five situations a paused, failed or pending payout is in, and take the one action that fixes each.
Use this recipe to find out which of five situations a paused, failed or pending payout is in, and take the one action that fixes it.
Who it is for. Any developer whose payout returned paused, sent a failed webhook, or sits in pending.
Status. Available.
Money example
- Ada sends 3,000.00 USD to her brother in Lagos.
- Her daily limit is 5,000.00 USD and she has already sent 3,750.00 USD today, so 1,250.00 USD is left.
- Gravv accepts the transfer but returns it
paused, withbreached_windowdailyandremaining_usd1250.00. Gravv pays nothing out. - You either resume it with proof of a higher verification level, or let it expire and send 1,250.00 USD now and the rest tomorrow.
Before you start
Store the reference and transaction_id of every transfer. Every check below starts from them.
Find out what is required
- The sender's limits.
GET /v1/customers/{customer_id}/limitsshows the caps and what is left, in USD (see Transfer limits). A 404not found: no limits policy appliesmeans your business has no limits policy. - Senders you verify yourself.
GET /v1/limits/remitters/{remitter_id}returns the same shape. - The pause itself. A paused transfer lists what it needs in
requirements[].
Steps
Find your situation below and follow it.
1. Paused for a limit
The response has pause_reason set to per_transfer, daily, weekly, monthly or annual:
{
"data": {
"amount": "3000",
"amount_currency": "USD",
"reference": "7d3f5b9e-2a4c-4e6f-8b0d-1c3e5a7b9d42",
"date_created": "2026-10-13T16:01:00.000Z",
"transfer_status": "paused",
"transaction_id": "4e6a8c0e-2f4b-4d6f-9a1c-3e5b7d9f1a53",
"fee": "1.5",
"fee_currency": "USD",
"requires_approval": false,
"status": "awaiting_remitter_information",
"park_reason": "remitter_information",
"pause_reason": "daily",
"requirements": [
{
"type": "remitter_verification",
"tier": "T2",
"levels": ["enhanced"],
"description": "resume with remitter_verification at one of these levels"
}
],
"breached_window": "daily",
"remaining_usd": "1250.00",
"expires_at": "2026-10-16T16:01:00Z"
},
"error": null
}
For remitter_verification, resume with your verification at one of the listed levels, and a new Idempotency-Key. Send only checks that you ran.
curl --request POST \
--url https://api.gravv.xyz/v1/transfer/7d3f5b9e-2a4c-4e6f-8b0d-1c3e5a7b9d42/resume \
--header 'Api-Key: <API_KEY>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: c775b886-915c-4b84-9973-861a6513bc1d' \
--data '
{
"remitter_verification": {
"remitter_id": "rem_ada_bello_01",
"level": "enhanced",
"provider": "in_house_kyc",
"provider_reference": "chk_88213",
"verified_at": "2026-10-13T15:30:00Z",
"screening": {
"sanctions": "clear",
"pep": "clear",
"adverse_media": "clear"
}
}
}
'
const response = await fetch("https://api.gravv.xyz/v1/transfer/7d3f5b9e-2a4c-4e6f-8b0d-1c3e5a7b9d42/resume", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "c775b886-915c-4b84-9973-861a6513bc1d",
},
body: JSON.stringify({
"remitter_verification": {
"remitter_id": "rem_ada_bello_01",
"level": "enhanced",
"provider": "in_house_kyc",
"provider_reference": "chk_88213",
"verified_at": "2026-10-13T15:30:00Z",
"screening": {
"sanctions": "clear",
"pep": "clear",
"adverse_media": "clear"
}
}
}),
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/transfer/7d3f5b9e-2a4c-4e6f-8b0d-1c3e5a7b9d42/resume",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
"Content-Type": "application/json",
"Idempotency-Key": "c775b886-915c-4b84-9973-861a6513bc1d",
},
json={
"remitter_verification": {
"remitter_id": "rem_ada_bello_01",
"level": "enhanced",
"provider": "in_house_kyc",
"provider_reference": "chk_88213",
"verified_at": "2026-10-13T15:30:00Z",
"screening": {
"sanctions": "clear",
"pep": "clear",
"adverse_media": "clear",
},
},
},
)
print(response.status_code, response.json())
{
"data": {
"transfer_id": "7d3f5b9e-2a4c-4e6f-8b0d-1c3e5a7b9d42",
"status": "approved_awaiting_execution",
"transfer_status": "pending",
"resumed": true
},
"error": null
}
For the other requirement types:
admin_review: only Gravv can release the transfer. Contact support with thereference, or let it expire and send withinremaining_usd.screening_clear: resume with aremitter_verificationwhosescreeningvalues are allclear.
2. Paused for sender details
If pause_reason is sender_details_required, resume with additional_information, as in Send money to Ghana.
3. Failed
The transfer.status.failed webhook arrives, and the transaction status is failed. Gravv has refunded the amount and the fee, as a transfer_refund row.
- Read
event_data.reasonif it is set, for examplelimit_pause_expiredorsender_details_expired. - Fix the cause.
- Send a new transfer with a new
Idempotency-Keyand a newclient_reference.
4. Manual review
The transfer.status.failed webhook arrives, but the transaction status is still pending. The outcome was unclear, so a person checks whether the money left before Gravv refunds anything. Do not send the transfer again. You could pay twice. Contact support with the reference.
5. A long pending
The route still reports "processing". Gravv never fails a transfer only because time has passed. If the transfer has been pending for an unusually long time for that country, contact support with the reference.
Missed webhooks
If your endpoint missed a webhook, find the deliveries for the transaction:
curl --request GET \
--url 'https://api.gravv.xyz/v1/webhooks/history?transaction_id=4e6a8c0e-2f4b-4d6f-9a1c-3e5b7d9f1a53&event_type=transfer' \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/webhooks/history?transaction_id=4e6a8c0e-2f4b-4d6f-9a1c-3e5b7d9f1a53&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=4e6a8c0e-2f4b-4d6f-9a1c-3e5b7d9f1a53&event_type=transfer",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
{
"data": {
"items": [
{
"id": "4e8a2c6f-1b3d-4f5a-9c7e-2d4f6a8c0e1b",
"transaction_id": "4e6a8c0e-2f4b-4d6f-9a1c-3e5b7d9f1a53",
"event_type": "transfer",
"status": "failed",
"path": "/hooks/gravv",
"http_method": "POST",
"response_status_code": 500,
"created_at": "2026-10-13T09:40:12.004Z",
"last_attempt": "2026-10-13T10:12:40.551Z"
}
],
"items_per_page": 50,
"page": 1,
"total_items": 1,
"total_pages": 1
},
"error": null
}
Here event_type is the category (transfer). One transfer has one row per event.
GET /v1/webhooks/event/{id}returns the attempts, the payload Gravv sent and your endpoint's response.GET /v1/webhooks/calls/{id}lists each attempt.
After you fix your endpoint, resend the event:
curl --request POST \
--url https://api.gravv.xyz/v1/webhooks/event/send/4e8a2c6f-1b3d-4f5a-9c7e-2d4f6a8c0e1b \
--header 'Api-Key: <API_KEY>'
const response = await fetch("https://api.gravv.xyz/v1/webhooks/event/send/4e8a2c6f-1b3d-4f5a-9c7e-2d4f6a8c0e1b", {
method: "POST",
headers: {
"Api-Key": process.env.GRAVV_API_KEY,
},
});
console.log(response.status, await response.json());
import os
import requests
response = requests.post(
"https://api.gravv.xyz/v1/webhooks/event/send/4e8a2c6f-1b3d-4f5a-9c7e-2d4f6a8c0e1b",
headers={
"Api-Key": os.environ["GRAVV_API_KEY"],
},
)
print(response.status_code, response.json())
Gravv does not sign the resend. An endpoint that requires the signature rejects it, so read the transaction to confirm its status.
What can go wrong
| Response | Why | Fix |
|---|---|---|
200 resumed: false with new requirements |
The transfer is still over the limit, or the level is not accepted | Send what requirements lists, or wait for the limit window to reset |
400 the remitter information does not match the sender this transfer was paused for; resume with the same remitter_id, or initiate a new transfer |
You sent a different remitter_id |
Use the same remitter_id |
400 remitter_verification.remitter_id is required |
A verification without an id | Add remitter_id |
422 transfers are not permitted for this sender |
The sender is blocked | Contact support. Do not retry |
503 transfer limits are temporarily unavailable, please retry |
Gravv cannot check limits right now | Retry |
409 the paused transfer changed while this request ran; please retry |
Two resume requests ran at once | Retry once with a new idempotency key |
404 transfer workflow not found |
You used the transaction_id as the id |
Use the reference |
400 this transfer is not paused, with a top-level transfer_status |
The transfer was already resumed, was never paused, or has finished | Read the transaction |
410 ERROR_CODE_TRANSFER_PAUSE_EXPIRED or ERROR_CODE_TRANSFER_PAUSE_CANCELLED |
The pause has ended | Send a new transfer |
409 client_reference has already been used for a different transfer … |
Your limits policy is enforced, and you reused a client_reference with a different body |
Use a new client_reference |
409 a transfer with this client_reference is still being processed; retry shortly, with Retry-After: 1 |
You retried too soon | Wait one second and retry |
409 idempotency_in_flight or 422 idempotency_key_reused |
A request with the same key is still running, or you used the key with a different body | Wait, or use a new key |
400 transaction not found |
The id is wrong, or it is from the other environment | Check the id and the API key |
400 event not found (webhooks) |
The id is not a history row's id, or it is from the other environment |
Use the history row's id |
Webhooks you will receive
transfer.requires_informationwhen a transfer pauses. It carriesreason, andlimit_detailswithbreached_window,cap_usd,used_usd,remaining_usdandrequirements.transfer.resumedafter a resume. Itsreasonisresumed, orreleasedwhen Gravv releases the transfer.- Then the usual
pending,completedorfailedevents. - When a pause ends,
transfer.status.failedwithreasonlimit_pause_expired,limit_pause_cancelled,sender_details_expiredorsupporting_document_expired.
Testing in sandbox
A new sandbox customer's limits policy is in shadow mode. You cannot produce a limit pause unless support sets a test policy, and sandbox transfers do not count toward usage.
Webhook history and the resend work in sandbox. Their data is separate from live.