Ingest financials

Push cash balances, transactions, and statements from any source.

This guide walks through populating the financials surface for a company. The endpoints are provider-agnostic — call them from a Mercury sync job, an OpenClaw export, a QuickBooks integration, or a one-off curl.

Prerequisites

  • A company id (co_...)
  • A human-scope API key
$export API=https://reventlov-dashboard.vercel.app
$export KEY=sk_...
$export CO=co_...

1. Post a cash balance

$curl -X POST $API/api/v1/companies/$CO/cash/balances \
> -H "Authorization: Bearer $KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "as_of": "2026-04-30",
> "balance_cents": 124500000,
> "currency": "USD",
> "source": "mercury"
> }'

Emits bank.balance.updated. The Cash on hand tile updates immediately. Re-post the same as_of to update the value (no upsert collision; multiple rows per day are allowed if connection_id differs).

2. Bulk-ingest transactions

Either send a single transaction, or wrap many in a transactions array. Provide an external_id per transaction to make the ingest idempotent — re-posts with the same (connection_id, external_id) are skipped.

$curl -X POST $API/api/v1/companies/$CO/bank/transactions \
> -H "Authorization: Bearer $KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "transactions": [
> {
> "external_id": "mercury_txn_001",
> "posted_at": "2026-04-29T14:23:00Z",
> "amount_cents": -395000,
> "description": "AWS",
> "category": "infrastructure",
> "counterparty": "Amazon Web Services"
> },
> {
> "external_id": "mercury_txn_002",
> "posted_at": "2026-04-28T18:00:00Z",
> "amount_cents": 1500000,
> "description": "Stripe payout",
> "category": "revenue",
> "counterparty": "Stripe"
> }
> ]
> }'

Emits a single bank.transaction.posted event with the count and the inserted rows. Up to 500 transactions per request.

3. Post a statement snapshot

$curl -X POST $API/api/v1/companies/$CO/financials/snapshots \
> -H "Authorization: Bearer $KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "statement_type": "income",
> "period_start": "2026-01-01",
> "period_end": "2026-03-31",
> "currency": "USD",
> "totals": {
> "revenue": 480000,
> "gross_profit": 360000,
> "operating_income": 90000,
> "net_income": 72000
> },
> "line_items": [
> { "key": "revenue", "label": "Revenue", "amount_cents": 480000, "is_total": true },
> { "key": "cogs", "label": "Cost of revenue", "amount_cents": -120000, "indent": 1 },
> { "key": "gross_profit", "label": "Gross profit", "amount_cents": 360000, "is_total": true },
> { "key": "opex", "label": "Operating expenses", "amount_cents": -270000, "indent": 1 },
> { "key": "operating_income", "label": "Operating income", "amount_cents": 90000, "is_total": true },
> { "key": "net_income", "label": "Net income", "amount_cents": 72000, "is_total": true }
> ],
> "source": "openclaw"
> }'

Emits financial.snapshot.updated. Re-posting the same (statement_type, period_end) upserts.

Repeat for statement_type=balance and statement_type=cashflow.

ResourceCadenceWhy
Cash balancehourlyCheap to fetch, drives the runway tile
Bank transactionsevery 15 min during business hoursCatches webhook misses
Income statementdailyStable mid-period, useful for pacing
Balance sheetdailySame
Cash flowdailySame
Full backfillonce at connection linkGet the last 12 months in one pass

Wiring it from a worker

A simple sync worker pseudocode:

1async function syncCompany(co: string) {
2 const balance = await mercury.getBalance();
3 await reventlov.post(`/api/v1/companies/${co}/cash/balances`, {
4 as_of: today(),
5 balance_cents: balance.amount_cents,
6 source: 'mercury',
7 });
8
9 const since = await reventlov.getLastTxnTime(co);
10 const txns = await mercury.getTransactions({ since });
11 if (txns.length) {
12 await reventlov.post(`/api/v1/companies/${co}/bank/transactions`, {
13 transactions: txns.map(toReventlovShape),
14 });
15 }
16}

Schedule this on Vercel Cron and let webhook events keep your dashboard revalidated on every push.