---
title: "Trace a transfer and resend a webhook"
description: "Find a transfer's current state, every event Gravv sent about it in order, whether your server accepted each one, and resend the one it missed."
---
Use this recipe to find a transfer's current state and every event Gravv sent about it, in order. You then see whether your server accepted each event, and you resend the one it missed. It is for a support engineer at a remittance app who must answer "I sent 250 USD to Ada on Monday. Where is it?"

**Status.** Available.

## Money example

On Monday at 09:15 you sent 250 USD to Ada.

1. Gravv recorded it and sent `transfer.status.pending`.
2. Gravv moved it through several steps and sent more `transfer.status.pending` events.
3. Ada's bank paid her, and Gravv sent `transfer.status.completed`. Your server returned 500, so your app still shows "sending".
4. You find that event, fix your server and resend it. Your app now shows "paid".

## Before you start

You have the `transaction_id` or the transfer `reference` from `POST /v1/transfer`. If you only have your `client_reference`, list the customer's rows with `GET /v1/transactions?customer_id=…` and find it there.

## Find out what is required

A transfer has three ids, and each route takes only one of them:

| Id | Where you got it | Used for |
| :-- | :-- | :-- |
| `transaction_id` | The `POST /v1/transfer` response | `GET /v1/transactions/{transaction_id}`; the `transaction_id` filter on webhook history |
| Transfer `reference` | The `POST /v1/transfer` response | Matches `event_data.reference` and `event_group_id` on webhooks; `POST /v1/transfer/{reference}/resume` |
| Webhook record `id` | `items[].id` from `GET /v1/webhooks/history` | The event detail, its delivery attempts and a resend |

## Steps

### 1. Read the current state

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url https://api.gravv.xyz/v1/transactions/1a2a73ed-562b-4f79-8374-59eef50bea63 \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/transactions/1a2a73ed-562b-4f79-8374-59eef50bea63", {
  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/1a2a73ed-562b-4f79-8374-59eef50bea63",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

Read `status`, `remark`, `fee` and `metadata.local_amount`. `completed` and `failed` are final.

### 2. List every event for it

`event_type` on webhook history is the **category** (`transfer`), not the full event name.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url 'https://api.gravv.xyz/v1/webhooks/history?transaction_id=1a2a73ed-562b-4f79-8374-59eef50bea63&event_type=transfer&limit=100' \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/webhooks/history?transaction_id=1a2a73ed-562b-4f79-8374-59eef50bea63&event_type=transfer&limit=100", {
  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=1a2a73ed-562b-4f79-8374-59eef50bea63&event_type=transfer&limit=100",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "items": [
      {
        "id": "3139d32c-93cd-49bf-9c94-1cf0dc98d2c1",
        "transaction_id": "1a2a73ed-562b-4f79-8374-59eef50bea63",
        "event_type": "transfer",
        "status": "failed",
        "path": "/hooks/gravv",
        "http_method": "POST",
        "response_status_code": 500,
        "created_at": "2026-10-12T14:02:11.120453Z",
        "last_attempt": "2026-10-12T14:03:12.004417Z"
      },
      {
        "id": "ddd1dfb2-3b98-4ef8-9af6-1a26146d3f31",
        "transaction_id": "1a2a73ed-562b-4f79-8374-59eef50bea63",
        "event_type": "transfer",
        "status": "sent",
        "path": "/hooks/gravv",
        "http_method": "POST",
        "response_status_code": 200,
        "created_at": "2026-10-12T09:15:23.512004Z"
      }
    ],
    "items_per_page": 100,
    "page": 1,
    "total_items": 2,
    "total_pages": 1
  },
  "error": null
}
```

The history list does not include the event payload. `status` is `pending`, `processing`, `sent`, `failed` or `retrying`.

### 3. See what an event said

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url https://api.gravv.xyz/v1/webhooks/event/3139d32c-93cd-49bf-9c94-1cf0dc98d2c1 \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/webhooks/event/3139d32c-93cd-49bf-9c94-1cf0dc98d2c1", {
  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/event/3139d32c-93cd-49bf-9c94-1cf0dc98d2c1",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

Read `payload.event_type` (here `transfer.status.completed`), `payload.event_data.status`, `payload.event_data.reason`, `payload.idempotency_key`, `response_status_code`, `response_body`, `attempts`, `max_retries` and `next_attempt`. On transfer events, `payload.idempotency_key` is `<reference>:<sequence>`, and a higher sequence means a later state.

### 4. See every delivery attempt

List the delivery attempts for the same webhook record `id`.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url https://api.gravv.xyz/v1/webhooks/calls/3139d32c-93cd-49bf-9c94-1cf0dc98d2c1 \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/webhooks/calls/3139d32c-93cd-49bf-9c94-1cf0dc98d2c1", {
  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/calls/3139d32c-93cd-49bf-9c94-1cf0dc98d2c1",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "calls": [
      {
        "id": "fc377a4c-4a15-444d-85e7-ce8a3a578a8e",
        "attempt_number": 1,
        "request_url": "https://payroll.example.com/hooks/gravv",
        "response_status": 500,
        "status": "failed",
        "response_time_ms": 88,
        "created_at": "2026-10-12T14:02:41.330127Z",
        "error_message": null
      }
    ]
  },
  "error": null
}
```

### 5. Resend it once your server is fixed

A resend takes no `Idempotency-Key` and no body. A scoped key needs `webhook:create`.

<CodeGroup>
```bash title="cURL"
curl --request POST \
     --url https://api.gravv.xyz/v1/webhooks/event/send/3139d32c-93cd-49bf-9c94-1cf0dc98d2c1 \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/webhooks/event/send/3139d32c-93cd-49bf-9c94-1cf0dc98d2c1", {
  method: "POST",
  headers: {
    "Api-Key": process.env.GRAVV_API_KEY,
  },
});
console.log(response.status, await response.json());
```
```python title="Python"
import os

import requests

response = requests.post(
    "https://api.gravv.xyz/v1/webhooks/event/send/3139d32c-93cd-49bf-9c94-1cf0dc98d2c1",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "id": "3139d32c-93cd-49bf-9c94-1cf0dc98d2c1",
    "tenant_id": "bdd640fb-0667-4ad1-9c80-317fa3b1799d",
    "event_category": "transfer",
    "target_url": "https://payroll.example.com/hooks/gravv",
    "status_code": 200,
    "duration_ms": 142,
    "response_body": { "received": true }
  },
  "error": null
}
```

<Warning>
The API returns 200 even when your server returned 500. Read `status_code`, which is your server's response. A manual resend carries **no `X-Signature`**. If your endpoint rejects unsigned calls, confirm the state another way, for example by reading the transaction again with `GET /v1/transactions/{transaction_id}`. Or wait for the automatic retries, which are signed. If your server rejects a resend, the event goes back into automatic retry.
</Warning>

### 6. Find the related rows

To find a refund, send `GET /v1/transactions?customer_id=5be6128e-18c2-4797-a142-ea7d17be3111&transaction_type=transfer_refund` and match `reference`. To find a fee, see [Add up a month of fees](/recipes/transactions/add-up-a-month-of-fees).

## What can go wrong

| Response | Why | Fix |
| :-- | :-- | :-- |
| 400 `transaction not found` | The id is wrong, belongs to another business, or belongs to the other environment | Check which key you used. Sandbox and live rows are separate |
| 400 `invalid transaction_id` | The id is not a UUID | Use the `transaction_id`, not the `reference` |
| History with `event_type=transfer.status.completed` is empty | The filter takes the category, not the event name | Send `event_type=transfer` |
| No way to filter history by `reference` | There is no such filter | Filter by `transaction_id` |
| You deduplicated on `X-Event-ID` and lost the `completed` event | `X-Event-ID` is the same for every event of one transfer | Deduplicate on the body's `event_id` |
| Events arrive out of order | Gravv delivers events concurrently | Apply an event only when its `idempotency_key` sequence is higher than the last one you applied |
| 400 `event is already being processed` on a resend | An automatic attempt is running | Wait, then try again |
| 400 `unsafe webhook url: webhook url must use https scheme` | Your webhook URL is `http` | Register an `https` URL in the Dashboard |
| 400 `event not found` | The id is not a history record id. For example, it is the event detail's `event_id` | Use `items[].id` from the history |
| 400 `invalid event ID format` | The id sent to the event detail route is not a UUID | Use `items[].id` from the history |
| An empty history for a `status` filter | An unknown `status` value returns nothing | Use `pending`, `processing`, `sent`, `failed` or `retrying` |
| 400 `circuit breaker open for webhooks: circuit breaker is open` | Gravv's webhook service is failing | Try again after about 30 seconds |

## Webhooks you will receive

For one transfer, Gravv sends events in this sequence order:

1. Several `transfer.status.pending` events, one per step.
2. Possibly `transfer.requires_information`, then `transfer.resumed`.
3. `transfer.status.completed` or `transfer.status.failed`.
4. Later, possibly `transfer.status.returned`.

Read `event_data.reference`, `client_reference`, `status`, `reason`, `associated_transactions` and `idempotency_key`. See [Transfer events](/platform/webhooks/event-catalogue#transfers).

## Testing in sandbox

Send a small sandbox transfer, point your sandbox webhook URL at an endpoint that returns 500, then run steps 2 to 5.

For example, Gravv sent 8 events for a 5 USD sandbox payout from a USDC wallet, with `idempotency_key` values `<reference>:1` through `<reference>:8`. The first seven were `transfer.status.pending`, and `transfer.status.completed` followed about 2 minutes later. Each event has its own `payload.event_id`. The event detail's top-level `event_id` is the transfer `reference`, so it is the same for every event. A resend returned 200 with the receiver's own `status_code`, added one to `attempts` and moved the event back to `retrying`.

## 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)
- [Webhook delivery attempts](/api-reference/webhooks/get-v1-webhooks-calls-id)
- [Resend a webhook event](/api-reference/webhooks/post-v1-webhooks-event-send-id)
