Build a report and capture a snapshot
Build a live report (account statement), capture an immutable snapshot with a hash, and export it to JSON/CSV.
Reports come in two kinds: live reports (which change along with the data) and snapshots (captured with a hash and reproducible). For statements and reconciliations, you capture a snapshot and then export it. For immutability and point-in-time balances, see Ledger.
The path from data to export:
Prerequisites
- The Vault's
codeand a period (from/to). - An access token (see Authentication).
- A snapshot id, for reading and exporting.
When to use a live report and when to use a snapshot:
| Live report | Snapshot | |
|---|---|---|
| data | current, changing | captured as of dataCutoff |
| reproducibility | no | yes (canonical sha256) |
| export | - | json / csv |
| use case | quick review | statements, reconciliation, audit |
Step 1. Build a live report
GET /api/v1/vaults/{code}/reports/account_statement builds a statement for a period.
curl "[BASE_URL]/api/v1/vaults/acme-otc/reports/account_statement?from=2026-03-01&to=2026-03-31&timezone=UTC" \
-H "Authorization: Bearer $V3_TOKEN"
const params = new URLSearchParams({ from: "2026-03-01", to: "2026-03-31", timezone: "UTC" });
const res = await fetch(
`[BASE_URL]/api/v1/vaults/acme-otc/reports/account_statement?${params}`,
{ headers: { Authorization: `Bearer ${process.env.V3_TOKEN}` } },
);
console.log(await res.json());
requests.get(
"[BASE_URL]/api/v1/vaults/acme-otc/reports/account_statement",
params={"from": "2026-03-01", "to": "2026-03-31", "timezone": "UTC"},
headers={"Authorization": f"Bearer {os.environ['V3_TOKEN']}"},
).json()
Statement parameters:
| Parameter | Required | Purpose |
|---|---|---|
from / to | yes | period boundaries |
timezone | no | time zone for day boundaries |
userId | no | admin: narrow down to a user |
breakdownBy=owner | no | breakdown by owner |
A live report reflects the current state of the data. Other reports are available too:
| Report | Request |
|---|---|
| account statement | GET .../reports/account_statement |
| approval activity | GET .../reports/approval_activity |
| flat operations register by filters | POST .../reports/query-operations |
Step 2. Capture a snapshot
POST /api/v1/vaults/{code}/report-snapshots generates the report, records the dataCutoff, and stores the result along with an operations manifest and a canonical sha256.
curl -X POST "[BASE_URL]/api/v1/vaults/acme-otc/report-snapshots" \
-H "Authorization: Bearer $V3_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reportType": "account_statement",
"from": "2026-03-01",
"to": "2026-03-31",
"timezone": "UTC"
}'
const snapshot = await fetch("[BASE_URL]/api/v1/vaults/acme-otc/report-snapshots", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.V3_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
reportType: "account_statement",
from: "2026-03-01",
to: "2026-03-31",
timezone: "UTC",
}),
}).then((r) => r.json());
snapshot = requests.post(
"[BASE_URL]/api/v1/vaults/acme-otc/report-snapshots",
headers={"Authorization": f"Bearer {os.environ['V3_TOKEN']}"},
json={
"reportType": "account_statement",
"from": "2026-03-01",
"to": "2026-03-31",
"timezone": "UTC",
},
).json()
A snapshot is immutable: viewing it again returns the same figures, even if operations, exchange rates, or names have changed since (retroactive matches, renames). A result that's too large (>~900KB) returns REPORT_PERIOD_TOO_LARGE; narrow the filters. Check the exact body schema in the API Reference.
Step 3. Find and read snapshots
| Action | Request |
|---|---|
list (filters: reportType, limit) | GET .../report-snapshots |
| single snapshot | GET .../report-snapshots/{id} |
| delete | DELETE .../report-snapshots/{id} |
| export | POST .../report-snapshots/{id}/exports |
Step 4. Export the snapshot
The export is built from the snapshot, not from a live query, so it always matches the captured figures:
curl -X POST "[BASE_URL]/api/v1/vaults/acme-otc/report-snapshots/SNAPSHOT_ID/exports" \
-H "Authorization: Bearer $V3_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "format": "csv" }'
await fetch("[BASE_URL]/api/v1/vaults/acme-otc/report-snapshots/SNAPSHOT_ID/exports", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.V3_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ format: "csv" }),
});
requests.post(
"[BASE_URL]/api/v1/vaults/acme-otc/report-snapshots/SNAPSHOT_ID/exports",
headers={"Authorization": f"Bearer {os.environ['V3_TOKEN']}"},
json={"format": "csv"},
)
Supported export formats:
| Format | Status |
|---|---|
json | supported |
csv | supported (flat operations register) |
xlsx | reserved → REPORT_UNSUPPORTED_FORMAT |
pdf | reserved → REPORT_UNSUPPORTED_FORMAT |
Currently, json and csv are supported. xlsx and pdf are reserved for now and return REPORT_UNSUPPORTED_FORMAT until a future release.
Common mistakes
| Symptom | Cause |
|---|---|
REPORT_PERIOD_TOO_LARGE | the result is > ~900KB; narrow the period or filters |
REPORT_UNSUPPORTED_FORMAT | xlsx/pdf was requested; only json/csv for now |
| figures "drift" between exports | the export came from a live query, not a snapshot |
| snapshot doesn't match expectations | check dataCutoff: data after it isn't included |
| empty report | from/to weren't set, or the period has no data |
You're done when
- the live report returns data for the period;
POST /report-snapshotsreturned a snapshot with adataCutoffand sha256;GET .../report-snapshots/{id}returns the same figures on repeated reads;- the export (
json/csv) is built from the snapshot and matches it.