API reference

A multi-currency, double-entry, point-in-time-queryable ledger. Sign up for an API key, post a balanced transaction with curl, query the balance at any past timestamp.

Base URL: https://api.rithvikronaldo.dev

Quickstart

Sixty seconds, four steps.

  1. 1. Sign up at the dashboard and copy the ac_… key.
  2. 2. Create your first account:
curl -X POST https://api.rithvikronaldo.dev/agents \
  -H "Authorization: Bearer ac_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "treasury", "currency": "USD"}'
  1. 3. Within five seconds the dashboard's left rail shows your new account.
  2. 4. Post a balanced transaction (see Transactions).

Authentication

Every authenticated request carries an Authorization: Bearer ac_… header. The middleware looks up the tenant by SHA-256 hash of the key and stashes the tenant id on the request. Every handler downstream reads from there.

Requests without a Bearer header resolve to the public demo tenant — that's what the unsigned dashboard sees.

The raw key is returned exactly once at signup. We never store the raw value; the database only sees the hash. If the DB leaks, the keys aren't usable directly.

Tenants & signup

POST/tenants

Create a tenant — or, if the email already exists, rotate its API key and return the existing tenant. Idempotent on email. The `created` flag distinguishes a fresh signup (true) from a rotation (false). The raw key is shown once; the database only stores its SHA-256 hash.

curl -X POST https://api.rithvikronaldo.dev/tenants \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com", "name": "Acme"}'
Response
{
  "tenant": {
    "id": "8f3e…",
    "email": "you@example.com",
    "name": "Acme",
    "created_at": "2026-05-18T09:12:43Z"
  },
  "api_key": "ac_REPLACE_WITH_YOUR_KEY",
  "created": true,
  "api_key_warning": "Save this key — it will not be shown again."
}
GET/tenants/me

Returns the tenant the current Bearer key resolves to. Falls back to the demo tenant for unauthenticated requests.

curl https://api.rithvikronaldo.dev/tenants/me \
  -H "Authorization: Bearer ac_YOUR_KEY"
Response
{
  "id": "8f3e…",
  "email": "you@example.com",
  "name": "Acme",
  "created_at": "2026-05-18T09:12:43Z"
}

Accounts

Accounts are the leaves of the ledger. An account has a code, a currency, and a type (asset, liability, revenue, etc.). Entries write to accounts in their native currency.

POST/agents

Create an account. Returns the account code derived from the new account's UUID.

curl -X POST https://api.rithvikronaldo.dev/agents \
  -H "Authorization: Bearer ac_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "treasury", "currency": "USD"}'
Response
{
  "id": "c8b25369-…",
  "name": "treasury",
  "status": "active",
  "currency": "USD",
  "account_code": "agent_c8b25369_usd",
  "created_at": "2026-05-18T09:13:01Z"
}
GET/agents

List the accounts under the current tenant.

curl https://api.rithvikronaldo.dev/agents \
  -H "Authorization: Bearer ac_YOUR_KEY"

Transactions

Every transaction is a set of entries that sum to zero per currency. The invariant Σ in = Σ out is enforced twice — in Go before the write, and again at COMMIT by a Postgres trigger.

Amounts are integer minor units. 1000000 means ₹10,000.00 (paise) or $10,000.00 (cents) depending on the row's currency.

POST/transactions

Post a balanced transaction. Use the Idempotency-Key header so retries don't double-write. A request with a known key replays the original response and adds Idempotent-Replay: true.

curl -X POST https://api.rithvikronaldo.dev/transactions \
  -H "Authorization: Bearer ac_YOUR_KEY" \
  -H "Idempotency-Key: settle_2026_05_29_batch_001" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Card settlement — vendor split",
    "occurred_at": "2026-05-29T14:32:00Z",
    "entries": [
      {"account": "cash",         "amount": 100000, "currency": "USD", "direction": "out"},
      {"account": "vendor_pool",  "amount":  85000, "currency": "USD", "direction": "in"},
      {"account": "fees",         "amount":  13000, "currency": "USD", "direction": "in"},
      {"account": "reserve",      "amount":   2000, "currency": "USD", "direction": "in"}
    ]
  }'
GET/transactions

List transactions for the current tenant, newest first. Cursor pagination via the `cursor` query param.

curl "https://api.rithvikronaldo.dev/transactions?limit=50" \
  -H "Authorization: Bearer ac_YOUR_KEY"
Error semantics
  • 422 unbalanced — Σ in ≠ Σ out per currency. Body has currency, in, out, diff.
  • 422 idempotency_hash_mismatch — same key, different body. Client bug — fix and retry.
  • 409 idempotency_pending — a request with the same key is still processing.
  • 200 OK + Idempotent-Replay: true — retry succeeded; original response replayed.

Authorize → capture → void

Two-stage money movement. Authorize reserves funds (creates an on_hold amount distinct from the realised balance). Capture confirms a portion (or all) and releases the rest. Void releases the whole reservation without capturing.

POST/authorizations

Reserve an amount from source to dest.

curl -X POST https://api.rithvikronaldo.dev/authorizations \
  -H "Authorization: Bearer ac_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "agent_<id>_usd",
    "dest":   "vendor_pool_usd",
    "amount": 500,
    "currency": "USD",
    "description": "subscription"
  }'
POST/authorizations/:id/capture

Capture a portion of the reserved amount.

curl -X POST https://api.rithvikronaldo.dev/authorizations/<auth.id>/capture \
  -H "Authorization: Bearer ac_YOUR_KEY" \
  -d '{"amount": 430}'
POST/authorizations/:id/void

Release the entire reservation. Idempotent.

curl -X POST https://api.rithvikronaldo.dev/authorizations/<auth.id>/void \
  -H "Authorization: Bearer ac_YOUR_KEY" \
  -d '{}'

Point-in-time balances

GET /accounts/:code/balance returns three numbers: balance (realised), available (balance minus pending authorizations), and on_hold (sum of pending). The as_of and in query params do the time-travel and FX conversion.

GET/accounts/:code/balance

Balance now, in the account's native currency.

curl https://api.rithvikronaldo.dev/accounts/treasury/balance \
  -H "Authorization: Bearer ac_YOUR_KEY"
GET/accounts/:code/balance?as_of=&in=

Balance at a past timestamp, converted to a target currency using the FX rate that was current at that timestamp.

curl "https://api.rithvikronaldo.dev/accounts/treasury/balance?as_of=2026-04-21T14:32:00Z&in=EUR" \
  -H "Authorization: Bearer ac_YOUR_KEY"
Response
{
  "account": "treasury",
  "currency": "EUR",
  "balance": 11891,
  "as_of": "2026-04-21T14:32:00Z"
}

Stress / load test

Bulk-post N balanced transactions through the same ledger.Post() primitive every other write uses. No fast path, no fixtures — real entries, real commits, same invariant check. The endpoint is rate-limited to signed-in tenants and capped at N = 10,000 per call. Used by the dashboard's ▶ Stress 1k button and by anyone wanting to measure their own ledger's throughput honestly.

POST/stress

Run a bulk-post stress test against the current tenant. `n` is the number of transactions to post; `concurrency` (optional, default 1) spreads them across N goroutines. Returns aggregate timing + the invariant-violations counter (always 0 by construction — every Post() runs CheckBalanced + the deferred Postgres trigger).

curl -X POST https://api.rithvikronaldo.dev/stress \
  -H "Authorization: Bearer ac_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"n": 1000, "concurrency": 4}'
Response
{
  "n_posted": 1000,
  "elapsed_ms": 2037,
  "tps_peak": 491.2,
  "p50_commit_ms": 7.9,
  "p99_commit_ms": 14.5,
  "invariant_violations": 0,
  "serialization_retries": 0,
  "currency": "USD"
}

Numbers above are from an M-series local + Postgres in Docker. serialization_retries stays at 0 because ledger.Post() is READ COMMITTED + append-only — no read-modify-write on account rows, no FOR UPDATE, nothing for Postgres to abort and retry. Contention shows up as lock-wait latency, not 40001 errors.

Events stream (SSE)

Server-Sent Events. Subscribe with any HTTP client that speaks text/event-stream. Auto-reconnects from the browser; the dashboard uses this for the live transaction stream.

GET/events/stream

Stream of ledger events for the current tenant. Demo tenant by default; sign in to filter to your own.

curl -N https://api.rithvikronaldo.dev/events/stream

Multi-tenancy

Every signup creates a tenant. Every row in accounts, transactions, authorizations, idempotency_keys, and agents carries a tenant_id. Every query filters by it. Cross-tenant isolation is enforced by predicate, tested by go test -run CrossTenant (five integration tests covering read and write paths).

Trade-off chosen over Postgres Row-Level Security: the predicate lives in the SQL where EXPLAIN can see it, not in a policy that has to be reverse-engineered when a row goes missing. RLS ties the predicate to a session variable inside a transaction; pgx pools recycle connections; one leaked SET LOCAL away from a cross-tenant read. The predicate written in every SQL string is uglier but grep-able and code-reviewable.