---
title: "Reconcile a day of movements"
description: "Match every movement dated yesterday to your own records and split the movements into final and still open, even though the list has no date filter."
---
This recipe matches every movement dated yesterday to your own records. It splits the movements into final and still open, even though the list has no date filter. It is for the finance team of a payroll app that pays contractors from a USD account on Gravv and closes its books every morning.

**Status.** Available.

## Money example

On 12 October 2026, the app sent three payouts from its USD account.

1. The 250 USD payout to Ada, with a 1.50 USD fee, is `completed`. Book it as paid.
2. The 400 USD payout to Bayo is `failed`, and a 400 USD `transfer_refund` row brought the money back. Book it as refunded. The money went out and came back in, so the net is zero.
3. The 120 USD payout to Kemi is still `pending`. Leave it open and check again tomorrow.

## Before you start

- For every transfer you sent, you saved the `reference` and `transaction_id` from `POST /v1/transfer`, and your own `client_reference`.
- You know the id of the account to reconcile (from `GET /v2/accounts`).

## Find out what is required

These are the transaction row fields you use to reconcile:

| Field | Use |
| :-- | :-- |
| `id` | The `transaction_id` that `POST /v1/transfer` returned |
| `reference` | The transfer `reference`. A refund row carries the same `reference` as the transfer it refunds |
| `client_reference` | Your own reference, as you sent it |
| `status` | `completed` and `failed` are final. `pending`, `paused` and `waiting_approval` are open |
| `transaction_type`, `direction`, `amount`, `currency`, `fee`, `date_created` | What to book |
| `remark` | Why a row failed or is held, in words you can show a customer |

## Steps

### 1. Pull the day, newest first

The list has **no date filter**. The API ignores `start_date` and `end_date` and returns no error. Page from the newest row, and stop at the first row older than the day you are reconciling.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url 'https://api.gravv.xyz/v1/transactions?account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&items_per_page=100&page=1' \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/transactions?account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&items_per_page=100&page=1", {
  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?account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&items_per_page=100&page=1",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response (rows trimmed)"
{
  "data": {
    "items": [
      {
        "id": "1a2a73ed-562b-4f79-8374-59eef50bea63",
        "customer_id": "5be6128e-18c2-4797-a142-ea7d17be3111",
        "amount": "250",
        "date_created": "2026-10-12T14:02:10.4Z",
        "direction": "out",
        "transaction_type": "global_remittance",
        "description": "Contractor payout October",
        "sending_account_id": "43b7a3a6-9a8d-4a03-980d-7b71d8f56413",
        "receiving_account_id": null,
        "reference": "371ecd7b-27cd-4130-8722-9389571aa876",
        "channel": "bank_transfer",
        "status": "completed",
        "remark": null,
        "client_reference": "pay-ada-2026-10",
        "fee": "1.5",
        "tx_hash": null,
        "metadata": { "recipient_name": "Ada Obi", "local_amount": "382500", "local_amount_currency": "NGN" },
        "balance_before": "10000",
        "balance_after": "9748.5",
        "currency": "USD"
      },
      {
        "id": "ab9099a4-35a2-40ae-9af3-05535ec42e08",
        "amount": "400",
        "date_created": "2026-10-12T11:30:45.1Z",
        "direction": "in",
        "transaction_type": "transfer_refund",
        "reference": "7412b293-4729-4739-a14f-f3d719db3ad0",
        "status": "completed",
        "currency": "USD"
      },
      {
        "id": "29a3b2e9-5d65-4441-9588-42dea2bc372f",
        "amount": "400",
        "date_created": "2026-10-12T09:12:03.7Z",
        "direction": "out",
        "transaction_type": "global_remittance",
        "reference": "7412b293-4729-4739-a14f-f3d719db3ad0",
        "status": "failed",
        "remark": "The recipient's account details are invalid. Your transfer has been refunded.",
        "client_reference": "pay-bayo-2026-10",
        "currency": "USD"
      }
    ],
    "page": 1,
    "items_per_page": 3,
    "total_pages": 1,
    "total_items": 3
  },
  "error": null
}
```

Paging rules:

- `items_per_page` in the response is the number of rows returned, not the number you asked for.
- `total_items` is an estimate when there are several pages.
- Request the next page (`page` + 1) until a page comes back empty or a row's `date_created` is before `2026-10-12T00:00:00Z`.

### 2. Match each row

For each row of the day, match `id` to your stored `transaction_id`. If no `id` matches, match `reference` to your stored transfer `reference`, then `client_reference`. A refund row (`transfer_refund`, `direction` `in`) matches the failed transfer by `reference`.

### 3. Classify

| Row | Book it as |
| :-- | :-- |
| `completed` | Paid |
| `failed`, with a `transfer_refund` row of the same `reference` | Refunded. The money went out and came back in |
| `pending`, `paused` or `waiting_approval` | Open. See [Tell why a transfer is stuck](/recipes/transactions/tell-why-a-transfer-is-stuck) |
| `pending` with the remark `Transfer is under review. Please contact support before trying again.` | Open, under review. **Do not send it again** |

### 4. Re-check the open rows the next day

Read each open row again by its `id`.

<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>

### 5. Optional: a quick CSV of the newest rows

The export holds **at most the 10 newest** matching rows. Use it for a quick look, never as the full day. Its dates must be `YYYY-MM-DD`, and `export_format` is lower case.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url 'https://api.gravv.xyz/v1/transactions/export?account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&start_date=2026-10-12&end_date=2026-10-12&export_format=csv' \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/transactions/export?account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&start_date=2026-10-12&end_date=2026-10-12&export_format=csv", {
  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/export?account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&start_date=2026-10-12&end_date=2026-10-12&export_format=csv",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

```json title="Response"
{
  "data": {
    "file_content": "aWQsZGF0ZV9jcmVhdGVkLGFtb3VudCxjdXJyZW5jeQo=",
    "file_name": "transactions_2026-10-13.csv",
    "content_type": "text/csv",
    "total_records": 10
  },
  "error": null
}
```

`file_content` is base64-encoded. The CSV columns are `id,date_created,amount,currency,direction,transaction_type,status,channel,description,fee,reference,tx_hash,sending_account_id,receiving_account_id,client_reference,balance_before,balance_after`. If you leave out `export_format`, you get JSON. The response then has `content_type` `application/json` and a `file_name` ending in `.json`, and `file_content` is `W10=` (an empty list) when nothing matches.

### 6. Optional: a running total

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url 'https://api.gravv.xyz/v1/transactions/volume?sending_account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&direction=out&status=completed&transaction_type=global_remittance' \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/transactions/volume?sending_account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&direction=out&status=completed&transaction_type=global_remittance", {
  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/volume?sending_account_id=43b7a3a6-9a8d-4a03-980d-7b71d8f56413&direction=out&status=completed&transaction_type=global_remittance",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

The response, such as `{"data":{"total_volume":"18250.5"},"error":null}`, is an **all-time** total, because the volume endpoint has no date filter. It adds every currency and transaction type together unless you filter them, so it is not a daily figure.

### 7. Catch the webhooks your server missed

List the transfer events your endpoint never accepted on that day. A date-only `end_date` means midnight, so `2026-10-13` covers all of 12 October.

<CodeGroup>
```bash title="cURL"
curl --request GET \
     --url 'https://api.gravv.xyz/v1/webhooks/history?event_type=transfer&start_date=2026-10-12&end_date=2026-10-13&status=failed' \
     --header 'Api-Key: <API_KEY>'
```
```javascript title="Node"
const response = await fetch("https://api.gravv.xyz/v1/webhooks/history?event_type=transfer&start_date=2026-10-12&end_date=2026-10-13&status=failed", {
  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?event_type=transfer&start_date=2026-10-12&end_date=2026-10-13&status=failed",
    headers={
        "Api-Key": os.environ["GRAVV_API_KEY"],
    },
)
print(response.status_code, response.json())
```
</CodeGroup>

Resend each one as in [Trace a transfer and resend a webhook](/recipes/transactions/trace-a-transfer-and-resend-a-webhook).

## What can go wrong

| Response | Why | Fix |
| :-- | :-- | :-- |
| `start_date` and `end_date` on the list return every date | The list has no date filter and ignores them | Page through the list and stop on `date_created` |
| The export has only 10 rows | The export is a fixed page of 10 | Use the list |
| The export ignores your dates | Only `YYYY-MM-DD` works. The export ignores other formats | Send `start_date=2026-10-12` |
| 400 `create export writer: unknown export format: CSV` | The format is case-sensitive | Send `export_format=csv` |
| An empty list for a filter | The value is misspelled, such as `status=complete` | Use `pending`, `completed`, `failed`, `waiting_approval` or `paused` |
| An empty response with `total_items` 0 | No row matches. An unknown account id does not return an error | Check the id |
| 400 `invalid request params` | `page` or `items_per_page` is not a number, or `customer_id` or `account_id` is not a UUID | Fix the value |
| 400 `database operation failed` on the volume | The account id is not a UUID | Send a UUID |
| US ACH and wire payouts missing with `transaction_type=ach_transfer` | Gravv records US bank payouts as `bank_account` | Filter `transaction_type=bank_account` |
| One transfer has two rows, out and in | The transfer failed and Gravv refunded it | Book both rows. The net is zero |
| A `completed` transfer, then money comes back | The bank returned it after completion (`transfer.status.returned`). The first row stays `completed`, and Gravv adds a new incoming row | Book the return on its own |
| The webhook said `transfer.status.completed` but the row is still `pending` | The row's last update was delayed | Check again later. If it stays `pending`, send support the `reference` |
| `total_items` is more than you can page through | `total_items` is an estimate when there are several pages | Stop on an empty page |

## Webhooks you will receive

Gravv sends no webhook when you read transactions. For the movements themselves, Gravv sends `transfer.status.*` (transfers), `transfer.inflow.*` (deposits) and `inflow.onchain.*` (on-chain wallet deposits).

## Testing in sandbox

Create rows with a [simulated deposit](/recipes/accounts-v2/test-accounts-with-a-simulated-deposit) and a few sandbox transfers, then run the steps with your `grvSec_sandbox_` key. Sandbox rows never appear with a live key.

## Reference

- [List transactions](/api-reference/transactions/get-v1-transactions)
- [Get a transaction](/api-reference/transactions/get-v1-transactions-transaction-id)
- [Export transactions](/api-reference/transactions/get-v1-transactions-export)
- [Transaction volume](/api-reference/transactions/get-v1-transactions-volume)
- [Webhook history](/api-reference/webhooks/get-v1-webhooks-history)
