Waterflow

API routes

Every /v1 route the API serves today.

Base: the server’s own address (locally http://127.0.0.1:8000 with dev.sh, or :8080 for the compose stack). Every route needs Authorization: Bearer <session token> except those marked “no sign-in”. In a browser, only the server’s own origin is allowed (403 origin_not_allowed otherwise). The OpenAPI schema is at /v1/openapi.json.

Conventions: money in integer minor units; ids are UUIDs; editable rows carry version (mismatch → 409); creates take a client-made id and are idempotent; errors are {"detail": "<code>"}. Within /v1 routes and fields are only added.

The server and reference data

Method Path Notes
GET /v1/health {"ok":true}; no sign-in
GET /v1/instance Name, server_version, api_version, signup (invite/open), setup_needed; no sign-in. Clients check it first
GET /v1/currencies ISO 4217 codes and decimals

Getting in (no sign-in)

Method Path Notes
POST /v1/setup The first account, with the setup code from the log; becomes admin. Optional server_name
POST /v1/setup/restore Multipart: setup_code, file (a backup), optional purged (its purged_accounts.jsonl) → 202; the server restarts and restores it. The code isn’t used up. 403 setup_invalid once there’s an admin
POST /v1/auth/invite-check {"code"} → {"valid"}, for the join screen
POST /v1/auth/sign-up Username, password, language (en or vi; en if left out), timezone, invite_code (unless sign-up is open) → token
POST /v1/auth/sign-in Username, password, code (TOTP or recovery code) when two-factor is on, remember_device with it, device_token from an earlier remember → token, and device_token when asked
POST /v1/auth/reset A reset link’s token and a new password → token; turns two-factor off

Sign-in security (signed in)

Method Path Notes
POST /v1/auth/sign-out Ends this session
POST /v1/auth/sign-out-others Ends every other session and forgets every other remembered device; send this device’s device_token to keep it
GET /v1/auth/sessions Signed-in devices: id, device (Chrome · Windows, or null), created_at, last_used_at, current; this one first. Never a token
DELETE /v1/auth/sessions/{id} Ends that session (404 for one that isn’t yours)
POST /v1/auth/password Current and new password; ends other sessions
GET /v1/auth/two-factor enabled, recovery_codes_left
POST /v1/auth/two-factor/setup A new secret, its otpauth:// URI and QR code; nothing saved yet
POST /v1/auth/two-factor/enable Secret and a code from it → ten recovery codes
POST /v1/auth/two-factor/recovery-codes A current code → new recovery codes
POST /v1/auth/two-factor/disable A current code

Me (the signed-in user)

Method Path Notes
GET /v1/me Profile, including is_admin and idle_sign_out_minutes
PATCH /v1/me Username, display name, language, timezone (last write wins); idle_sign_out_minutes (1 to 1440, 5 by default, or null for never; left out leaves it)
GET /v1/me/status Open, or closed with deletion date
POST /v1/me/delete Close the account (30-day restore)
POST /v1/me/restore Restore a closed account
GET /v1/me/export ZIP of readable CSVs

The server’s admin

Method Path Notes
PATCH /v1/admin/instance Server name, signup mode
GET /v1/admin/users Accounts with last seen, status, two-factor
POST /v1/admin/users/{id}/disable {"disabled": true/false}; disabling ends their sessions
POST /v1/admin/users/{id}/reset A one-time reset token (24 hours)
GET, POST /v1/admin/invites POST: note, days (1 to 30), server (the admin’s origin) → code, link, QR
DELETE /v1/admin/invites/{id} Revoke
GET /v1/admin/jobs The daily jobs (purge, backup, rates) in the order they run: at_minute and timezone (null: JOBS_HOUR), default_minute, enabled, next_run_at, and the last run (last_started_at, last_finished_at, last_result done/failed, last_detail)
PATCH /v1/admin/jobs/{name} at_minute (0 to 1439) with its timezone, or null for JOBS_HOUR; enabled (only rates can be off: 422 invalid); what’s left out stays
GET /v1/admin/update Running and newest version, whether the updater runs (a recent running status counts), its last status (null when that’s from before the server started: an update by hand or a restart came since), changelog_url (from PROJECT_SITE_URL)
POST /v1/admin/update Ask the updater to update now (409 updater_off without it)
GET, POST /v1/admin/restore GET: the last restore from the web app (status requested/running/done/failed, at, detail), or null. POST, multipart: file, optional purged, password, code when two-factor is on, confirm (true) → 202; the server restarts and restores it

Books and their contents

Method Path Notes
GET, POST /v1/books POST creates with starter categories unless unticked
GET, PATCH, DELETE /v1/books/{book_id}
GET /v1/books/{book_id}/changes {"marker"}: changes whenever anything in the book does, by anyone. Poll it and refetch the book when it differs (the web app does every 5 seconds while it’s on screen)
GET /v1/books/{book_id}/dashboard Everything the home screen shows, in one call, with rates_source (some sources need a credit)
GET, POST /v1/books/{book_id}/accounts Each with its balance, converted_balance (in the book’s currency at today’s rate; null without a rate) and pinned (on this user’s Overview). POST takes an opening balance, optional contact_id and the details by kind. People’s own accounts are listed too (kind person, contact_id = their id); POST can’t make one
PATCH /v1/books/{book_id}/accounts/{id} Includes archive. A detail left out stays; null clears it. A person’s own account answers person_account
PUT, DELETE /v1/books/{book_id}/accounts/{id}/pin Pin it to this user’s Overview, or unpin it (204). Each member’s own; a viewer can too
GET /v1/books/{book_id}/accounts/{id}/bills A credit card’s or loan’s bills today (book’s timezone), or []: a card’s last statement while it has something to pay, then the next statement so far (statement_open: true, with only what’s new in it); a loan’s installment. Each: kind (card/loan), period_start, period_end, due_on, amount, paid, remaining, owed_now, interest and fee still to record with their fixed interest_transaction_id and fee_transaction_id, pay_from (the account of the last payment), statement_open; a card’s also installments the statement bills (plan_id, transaction_id, payee, number, months, amount), fees to record (plan_id, transaction_id, payee, amount) and limit_used. Paying is ordinary POST .../transactions
GET /v1/books/{book_id}/accounts/{id}/bill The first of .../bills (the one due now), or null
GET, POST /v1/books/{book_id}/categories
PATCH, DELETE /v1/books/{book_id}/categories/{id} DELETE archives a used category
GET, POST /v1/books/{book_id}/transactions Filters: account_id (with running balance), category_id (with sub-categories), from, to, type, q; newest first by date then time; pages by after. occurred_time: HH:MM, 00:00 if left out
GET /v1/books/{book_id}/transactions/suggest type (expense/income), amount → {account_id, category_id, payee} or null: from this user’s own transactions of the type, within 10 % of the amount, in the last 90 days, what makes up at least 2 of the last 3. The web app fills a new transaction’s untouched fields with it
GET, PUT, DELETE /v1/books/{book_id}/transactions/{id} PUT without occurred_time keeps the saved time
GET /v1/books/{book_id}/transactions/{id}/history Every saved version
GET, POST /v1/books/{book_id}/contacts With each person’s currency (of their own account, which has their id) and balances. POST takes currency (default: the book’s) and adds their account
PATCH, DELETE /v1/books/{book_id}/contacts/{id} Renaming or archiving a person does the same to their account. DELETE deletes their account too when nothing is recorded; otherwise, or when a lent or loan account links them, it archives both
GET, POST /v1/books/{book_id}/budgets With current progress
PATCH, DELETE /v1/books/{book_id}/budgets/{id}
GET, POST /v1/books/{book_id}/installments Credit card purchases paid in installments, with installment, billed and left_to_bill. POST: id, transaction_id (an expense on a card with a statement day), months (2 to 60), optional converted_on (the purchase’s date), first_statement (the statement the conversion date falls in), fee, fee_split, counts_toward_limit (true). 409 installment_exists for a purchase that has one
PATCH, DELETE /v1/books/{book_id}/installments/{id} PATCH takes version and any of the POST fields, and ended_on to end it early (null: not ended)

Account details by kind

All optional. One sent for a kind it doesn’t belong to is 422 invalid; changing an account’s kind clears the ones that stop applying.

Field Kinds Value
note all Text, up to 500 characters
credit_limit, annual_fee credit_card Minor units of the account’s currency, 0 or more
statement_day credit_card Day of the month, 1 to 31
payment_due_day credit_card, loan Day of the month, 1 to 31: a card’s payment due day, a loan’s payment day
installment loan The monthly payment, minor units, above 0
installment_fee loan A one-off fee, minor units, 0 or more; charged once, with the first payment
interest_rate_bp savings, credit_card, loan, lent Yearly rate in hundredths of a percent (550 is 5.5 %), 0 to 100000. Information only: it never changes a balance

Sharing a book

Method Path Notes
GET /v1/books/{book_id}/members Every member: name (username or “Deleted user”), display_name, role, pending, me
POST /v1/books/{book_id}/members Owners: username, role (default editor) → the member list. 409 cannot_invite for an unknown username and for someone already in the book, alike; rate limited
PATCH /v1/books/{book_id}/members/{user_id} Owners: role. 409 owner_required for the last owner
DELETE /v1/books/{book_id}/members/{user_id} Owners remove anyone or cancel an invite; anyone leaves with their own id. 409 owner_required for the last owner
GET /v1/me/book-invites Invites I haven’t answered: book, role, who invited me
POST /v1/me/book-invites/{book_id}/accept Become a member
DELETE /v1/me/book-invites/{book_id} Decline; the invite goes away, nobody is told

Source: waterflow/backend/app/api/routes/v1/. This page is the list: a new route comes with a paired docs PR that adds it here.

Edit this page · Report a problem