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.

  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.

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

Reference

Was this page helpful?