curl --request GET \
--url https://api.range.org/v2/reconciliation/roll-forward \
--header 'X-API-KEY: <api-key>'import requests
url = "https://api.range.org/v2/reconciliation/roll-forward"
headers = {"X-API-KEY": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-KEY': '<api-key>'}};
fetch('https://api.range.org/v2/reconciliation/roll-forward', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.range.org/v2/reconciliation/roll-forward",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-KEY: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.range.org/v2/reconciliation/roll-forward"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-KEY", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.range.org/v2/reconciliation/roll-forward")
.header("X-API-KEY", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.range.org/v2/reconciliation/roll-forward")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-KEY"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"requested_period": {
"start_time": "2026-07-01T00:00:00.000Z",
"end_time": "2026-08-01T00:00:00.000Z"
},
"reporting_currency": "USD",
"items": [
{
"account_id": "acc-1",
"status": "ok",
"lines": [
{
"asset": "USDC",
"opening_quantity": "100.5",
"movements_quantity": "20.25",
"movements": [
{
"kind": "trade_in",
"quantity": "25.00"
}
],
"actual_closing_quantity": "120.75",
"computed_closing_quantity": "120.75",
"difference": "0",
"declared_gap_reason": null,
"opening_usd": "100.50",
"closing_usd": "120.75"
}
],
"declared_gaps": [
{
"reason": "trade_quote_leg_unattributed",
"asset": "USDC",
"quantity": "500.00",
"transaction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
],
"effective_period": {
"start_time": "2026-07-01T00:00:00.000Z",
"end_time": "2026-08-01T00:00:00.000Z"
},
"reconcilable_from": "2026-07-23T12:00:00.000Z",
"unreconcilable_reason": null,
"unsynced_period": null
}
]
}Roll-forward balance reconciliation
For a requested period, ties each account/asset line’s opening balance observation plus in-between movements to its actual closing observation — transfers and trades alike. A trade’s proceeds are attributed to whichever account in the trade’s connection actually holds the asset received (not only the account the trade row is attributed to), so both legs of a trade tie; a quote or fee leg with no account anywhere in the connection is declared in declared_gaps rather than silently dropped. Each movement line is labeled by kind (transfer_in, transfer_out, trade_in, trade_out, trade_fee) alongside the net movements_quantity, supporting both a quantity-conservation reading and a flow reading. Each account’s effective period (the real observation timestamps actually used) is returned alongside the requested period, and may differ per account. The movement-count cap is evaluated once per connection (not per account, since a widened read spans every sibling); an affected connection returns movements_truncated and no computed closing for every account in it, rather than a partial number. USD accompanies each line for display only; it is never part of the identity or difference. Opening/closing balances and movements are both filtered through the workspace’s token whitelist, per asset leg, so a hidden token cannot manufacture a phantom break on one side only; pass show_all=true to bypass filtering on both sides. account_ids can be narrowed further with the group_id, provider, network, account_type, role, and connection_id filters — a requested id failing any provided filter is dropped from the statement the same way an id outside the caller’s workspace already is. reporting_currency accepts only USD today (no FX conversion exists yet); any other value is rejected.
curl --request GET \
--url https://api.range.org/v2/reconciliation/roll-forward \
--header 'X-API-KEY: <api-key>'import requests
url = "https://api.range.org/v2/reconciliation/roll-forward"
headers = {"X-API-KEY": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'X-API-KEY': '<api-key>'}};
fetch('https://api.range.org/v2/reconciliation/roll-forward', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.range.org/v2/reconciliation/roll-forward",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-KEY: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.range.org/v2/reconciliation/roll-forward"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-KEY", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.range.org/v2/reconciliation/roll-forward")
.header("X-API-KEY", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.range.org/v2/reconciliation/roll-forward")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-KEY"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"requested_period": {
"start_time": "2026-07-01T00:00:00.000Z",
"end_time": "2026-08-01T00:00:00.000Z"
},
"reporting_currency": "USD",
"items": [
{
"account_id": "acc-1",
"status": "ok",
"lines": [
{
"asset": "USDC",
"opening_quantity": "100.5",
"movements_quantity": "20.25",
"movements": [
{
"kind": "trade_in",
"quantity": "25.00"
}
],
"actual_closing_quantity": "120.75",
"computed_closing_quantity": "120.75",
"difference": "0",
"declared_gap_reason": null,
"opening_usd": "100.50",
"closing_usd": "120.75"
}
],
"declared_gaps": [
{
"reason": "trade_quote_leg_unattributed",
"asset": "USDC",
"quantity": "500.00",
"transaction_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
],
"effective_period": {
"start_time": "2026-07-01T00:00:00.000Z",
"end_time": "2026-08-01T00:00:00.000Z"
},
"reconcilable_from": "2026-07-23T12:00:00.000Z",
"unreconcilable_reason": null,
"unsynced_period": null
}
]
}Authorizations
Authorization method required to allow user to access the api endpoints.
Query Parameters
Requested period start (ISO 8601).
"2026-07-01T00:00:00Z"
Requested period end (ISO 8601).
"2026-08-01T00:00:00Z"
Comma-separated account ids (UUIDs) to reconcile. Ids outside the caller's workspace are silently dropped, as is any requested id that does not also match every filter dimension below — the filters narrow this list further, they never expand it. Accepts at most 100 ids; a longer list is rejected with a 400 rather than processed. A non-UUID entry is also rejected with a 400 naming the field, rather than reaching the database (RNG-5847).
1"a1b2c3d4-e5f6-7890-abcd-ef1234567890,b2c3d4e5-f6a7-8901-bcde-f12345678901"
Filter to accounts that belong to this group (account_groups.id). A non-UUID value is rejected with a 400 naming the field (RNG-5847).
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
Filter to accounts held under a connection of this provider.
utila, kraken, binance, okx, bybit, bitget, gate, kucoin, plaid, squads, altitude, coinbase, realms, wise, safe, hyperliquid, cubist, privy, dfns, anchorage, revolut_business, turnkey, fordefi, coins_ph, fireblocks, pave_bank, copper "utila"
Filter to accounts on this network (e.g. ethereum, solana, stellar).
"ethereum"
Filter to accounts of this account type.
eoa, multisig, contract, custodian, exchange, bank, defi "exchange"
Filter to accounts tagged with this free-text role.
"treasury"
Filter to accounts under this connection id. A non-UUID value is rejected with a 400 naming the field (RNG-5847).
"a1b2c3d4-e5f6-7890-abcd-ef1234567890"
Currency the statement's quantities/USD figures are denominated in. Only 'USD' is supported today — no FX conversion exists yet, so any other value is rejected with a 400. Ships now so downstream consumers see the final request shape ahead of multi-currency support landing. Validated for membership against a known currency set, and bounded to a short length — an unsupported or oversized value is rejected with a message derived from the caller's own input.
"USD"
Restores every opening/closing balance line and movement to its unfiltered shape, ignoring the workspace token whitelist on both sides of the identity. Never mutates stored overrides.
Response
Show child attributes
Show child attributes
Currency every quantity/USD figure in this response is denominated in. Always 'USD' today — echoed back so downstream consumers can rely on the field once multi-currency support lands, without a breaking change (RNG-5665).
"USD"
Show child attributes
Show child attributes
Was this page helpful?