Reconcile a day of movements
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.
- The 250 USD payout to Ada, with a 1.50 USD fee, is
completed. Book it as paid. - The 400 USD payout to Bayo is
failed, and a 400 USDtransfer_refundrow brought the money back. Book it as refunded. The money went out and came back in, so the net is zero. - 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
referenceandtransaction_idfromPOST /v1/transfer, and your ownclient_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.
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>'
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());
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())
{
"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_pagein the response is the number of rows returned, not the number you asked for.total_itemsis an estimate when there are several pages.- Request the next page (
page+ 1) until a page comes back empty or a row'sdate_createdis before2026-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 |
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.
curl --request GET \
--url https://api.gravv.xyz/v1/transactions/29a3b2e9-5d65-4441-9588-42dea2bc372f \
--header 'Api-Key: <API_KEY>'
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());
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())
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.
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>'
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());
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())
{
"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
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>'
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());
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())
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.
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>'
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());
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())
Resend each one as in 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 and a few sandbox transfers, then run the steps with your grvSec_sandbox_ key. Sandbox rows never appear with a live key.