---
title: "Tell why a transfer is stuck"
description: "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.

1. Gravv is still waiting on the bank or the mobile-money operator. Wait.
2. It is paused because sender details are missing. Send the details and resume it, and the transfer continues.
3. It is under review. The money may already have left, so do **not** send it again. Contact support.
4. 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

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url https://api.gravv.xyz/v1/transactions/29a3b2e9-5d65-4441-9588-42dea2bc372f \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
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());
```
```python title="Python"
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())
```
</CodeGroup>

```json title="Response (trimmed)"
{
  "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`.

<CodeGroup>
```bash title="cURL"
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>'
```
```javascript title="Node"
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());
```
```python title="Python"
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())
```
</CodeGroup>

### 3. If it is paused, answer the pause

Use [Check what a sender can still send](/recipes/transactions/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](/recipes/remittance-and-crypto/get-a-stuck-remittance-moving#1-paused-for-a-limit) shows each kind of pause and its resume body.

### 4. If it failed, confirm the refund

List the customer's `transfer_refund` rows.

<CodeGroup>
```bash title="cURL"
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>'
```
```javascript title="Node"
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());
```
```python title="Python"
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())
```
</CodeGroup>

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_information` for a pause, with `limit_details`.
- `transfer.resumed`, with `reason` `resumed` or `released`.
- `transfer.status.completed`.
- `transfer.status.failed`, with a `reason` when 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.

## Reference

- [Get a transaction](/api-reference/transactions/get-v1-transactions-transaction-id)
- [Webhook history](/api-reference/webhooks/get-v1-webhooks-history)
- [Webhook event](/api-reference/webhooks/get-v1-webhooks-event-id)
- [Resume a transfer](/api-reference/transfer/post-v1-transfer-id-resume)
- [Paused transfers](/platform/transfers/paused-transfers)
