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

  1. Ada sends 3,000.00 USD to her brother in Lagos.
  2. 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.
  3. Gravv accepts the transfer but returns it paused, with breached_window daily and remaining_usd 1250.00. Gravv pays nothing out.
  4. 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

  1. The sender's limits. GET /v1/customers/{customer_id}/limits shows the caps and what is left, in USD (see Transfer limits). A 404 not found: no limits policy applies means your business has no limits policy.
  2. Senders you verify yourself. GET /v1/limits/remitters/{remitter_id} returns the same shape.
  3. 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 the reference, or let it expire and send within remaining_usd.
  • screening_clear: resume with a remitter_verification whose screening values are all clear.

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.

  1. Read event_data.reason if it is set, for example limit_pause_expired or sender_details_expired.
  2. Fix the cause.
  3. Send a new transfer with a new Idempotency-Key and a new client_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_information when a transfer pauses. It carries reason, and limit_details with breached_window, cap_usd, used_usd, remaining_usd and requirements.
  • transfer.resumed after a resume. Its reason is resumed, or released when Gravv releases the transfer.
  • Then the usual pending, completed or failed events.
  • When a pause ends, transfer.status.failed with reason limit_pause_expired, limit_pause_cancelled, sender_details_expired or supporting_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.

Reference

Was this page helpful?