API reference
Every screen is already an endpoint
The app is server-rendered, so there is no second API to keep in sync. A personal token authenticates a script against the same routes your browser uses.
How it works
Authentication
The host in every example below is your own instance — the machine you self-host on, or your hosted subdomain. The public demo at demo.pathayam.app lets anyone in without an account and issues no tokens, so these examples will not run against it.
One header on every request: Authorization: Bearer bgt_…. Tokens are minted in Settings — never while viewing as another member — scoped read or read-write, and shown exactly once. One expires after a whole number of days from 1 to 3650, or never, and stops working the moment its member is removed.
Idempotency
Every write accepts Idempotency-Key. Replaying a key returns the original result instead of acting twice, so a timeout you never saw is safe to retry. The same key with a different body is refused with 422.
Responses
Reads return HTML — this is the same app your browser talks to. /export.json, the CSVs and /healthz return data. Writes redirect on success. Send Accept: application/json and a write answers { "redirect": …, "message": … } instead, an error comes back as { "error": … } with its status, and a request that is not signed in gets 401 rather than a redirect to the sign-in page.
YYYY-MM-DD (or DD-MM-YYYY, the way the app shows them)
and months are YYYY-MM, both in IST; a month more than a hundred years from the rest of the
budget is refused. Send fields form-encoded, as multipart, or as a flat JSON object — a number or
true is read as the text a form would send, and a nested object is ignored.
Budget & money
Everything that moves money between envelopes. These are the routes that change what your budget says.
GET / The month grid
The budget screen for a month: groups, categories, assigned, activity and available, with Ready to Assign and the digest.
Scope
read or higher
Parameters
| Name | Type | Description | |
|---|---|---|---|
month | string | optional | YYYY-MM. Defaults to the current month in IST. |
budget | string | optional | Which budget, in a household with more than one. Defaults to the last one viewed. |
Responses
- 200 — The rendered screen, as HTML.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 429 — Rate limit exceeded. Retry after the interval in the response.
Example
curl -sS https://pathayam.yourdomain.com/?month=2026-09 \
-H "Authorization: Bearer bgt_…"
POST /assign Assign to a category
Set the amount assigned to one category in one month. This is an absolute value, not a delta — sending 4500 makes the assignment 4500, whatever it was before. A month too far back to take an assignment is refused.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
month | string | optional | YYYY-MM. Defaults to the current month in IST. |
category_id | string | required | The category receiving the money. |
amount | number | optional | Rupees. Accepts an expression such as 450+120. Empty sets it to 0. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/assign \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" \
-d "month=2026-09&category_id=cat_7f3&amount=4500"
POST /move Move between envelopes
Move money from one category to another within the same month. Ready to Assign is unaffected — the total assigned does not change, only where it sits. This is how you cover an overspend. Both envelopes must be in the same budget — money between budgets is a transfer between their accounts, and a move is refused.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
month | string | optional | YYYY-MM. Defaults to the current month in IST. |
from_category_id | string | required | Where the money comes from. rta takes it from Ready to Assign. |
to_category_id | string | required | Where it goes. |
amount | number | required | Rupees. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/move \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" \
-d "month=2026-09&from_category_id=cat_hol&to_category_id=cat_eat&amount=1320"
POST /hold Hold income for next month
Take an amount out of this month's Ready to Assign and place it at the top of next month's. Send 0 to release everything currently held.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
month | string | optional | The month holding the money back. Defaults to the current month in IST. |
budget | string | optional | Which budget, in a household with more than one. Defaults to the last one viewed. |
amount | number | optional | Rupees. Empty or 0 releases the hold. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/hold \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" \
-d "month=2026-09&amount=139900"
GETPOST /auto-assign Fund every target
GET previews what funding all targets would do. POST commits it. Money is spent down the budget in your own order, so a short month funds the top of the list first. It funds one budget, from that budget's Ready to Assign.
Scope
read to preview, read-write to commit
Parameters
| Name | Type | Description | |
|---|---|---|---|
month | string | optional | YYYY-MM. Defaults to the current month in IST. |
budget | string | optional | Which budget, in a household with more than one. Defaults to the last one viewed. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS https://pathayam.yourdomain.com/auto-assign?month=2026-09 \
-H "Authorization: Bearer bgt_…"
GET /explain/ready-to-assign Why that number
How Ready to Assign for a month is made up: income received to date, less what is assigned this month and earlier, what is assigned in future months, what is held for next month, and any cash overspending carried in.
Scope
read or higher
Parameters
| Name | Type | Description | |
|---|---|---|---|
month | string | optional | YYYY-MM. Defaults to the current month in IST. |
budget | string | optional | Which budget, in a household with more than one. Defaults to the last one viewed. |
Responses
- 200 — The rendered screen, as HTML.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 429 — Rate limit exceeded. Retry after the interval in the response.
Example
curl -sS https://pathayam.yourdomain.com/explain/ready-to-assign?month=2026-09 \
-H "Authorization: Bearer bgt_…"
More budget & money routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /move | read | month, to, amount | The move form, with the destination and amount filled in when given. |
| GET | /hold | read | month | The hold form. |
| POST | /copy-last-month | read-write | month | Copy last month's assignments into this one. |
| GET | /explain/category/:id | read | month | How one category's available figure is made up. |
| GET | /overview | read | — | Runway, bills due soon and the month at a glance. |
| POST | /budgets/personal | read-write | — | Create your own personal budget if you do not have one, with its household envelope and a few starting envelopes. |
| GET | /more | read | — | The hub of everything not in the main navigation. |
| GET | /setup | read | — | The first-run screen: start from a template or blank. |
| POST | /setup | read-write | monthly_income, has_emis, has_school_fees, has_domestic_help (each 1 for yes) | Create starting categories from a template. Then to /accounts/new. |
| POST | /setup/blank | read-write | — | Start with no categories. Then to /accounts/new. |
Accounts & cards
The accounts money sits in, and the cards drawn on them. An account is closed, never deleted.
GET /accounts List accounts
The accounts of the budget being viewed, plus the tracking accounts, with cleared, uncleared and working balances, grouped into budget, credit and tracking.
Scope
read or higher
Responses
- 200 — The rendered screen, as HTML.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 429 — Rate limit exceeded. Retry after the interval in the response.
Example
curl -sS https://pathayam.yourdomain.com/accounts \
-H "Authorization: Bearer bgt_…"
GET /cards Cards by due date
Every credit account in the budget being viewed, ordered by what falls due next, with what is owed, what the payment envelope holds, and anything unfunded.
Scope
read or higher
Responses
- 200 — The rendered screen, as HTML.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 429 — Rate limit exceeded. Retry after the interval in the response.
Example
curl -sS https://pathayam.yourdomain.com/cards \
-H "Authorization: Bearer bgt_…"
More accounts & cards routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /accounts/new | read | — | The new-account form. |
| POST | /accounts/new | read-write | name, subtype (required); kind, institution, last4, opening_balance, opening_date, statement_day, due_day, budget_id, visibility; deposit terms as below | Create an account. |
| GET | /accounts/:id | read | card (one card's spending) | One account's register. |
| POST | /accounts/:id/edit | read-write | name (required); last4, statement_day, due_day, visibility | Rename an account or change its details. |
| POST | /accounts/:id/close/accounts/:id/reopen | read-write | — | Close an account (its history is untouched) or reopen it. |
| POST | /accounts/:id/deposit | read-write | deposit_kind (fd, rd, ppf, epf, other), deposit_principal, deposit_instalment, deposit_rate (a percentage), deposit_compounding, deposit_start_date, deposit_maturity_date, deposit_payout (cumulative or periodic) | Set or change a deposit's terms, so it values itself. A fixed deposit, recurring deposit or other-asset tracking account only; refused (422) otherwise. A hand value dated on or after the basis still wins. |
| POST | /accounts/:id/deposit/remove | read-write | — | Take a deposit's terms off; it is worth its balance again. |
| GET | /accounts/:id/statement | read | — | The card-statement form. |
| POST | /accounts/:id/statement | read-write | amount, due_date (required); statement_date, minimum_due | Record a credit-card statement, so funding advice uses the real bill. |
| GET | /accounts/:id/reconcile | read | — | The reconcile screen. |
| POST | /accounts/:id/reconcile | read-write | bank_balance, as_of, clear (transaction ids, repeated), allow_adjustment | Reconcile to a statement balance, clearing the transactions named. A difference is never guessed: it is shown, and booked as an adjustment only with allow_adjustment=1. |
| GET | /accounts/:id/cards | read | — | Cards on one account, including add-ons. |
| POST | /accounts/:id/cards | read-write | label (required); last4, holder_member_id | Add an add-on card to an account. |
| POST | /accounts/:id/cards/:cardId/close | read-write | — | Close an add-on card. 404 unless the card is visible and on that account. |
Transactions
Where money actually arrives and leaves. Every change here is an event you can undo from Activity.
POST /add Record a transaction
Create one transaction. Money out must name a category — the requirement is enforced on the server, not only in the form. Money in is exempt, because its job is to land in Ready to Assign. A cleared entry dated inside a reconciled period answers with the “Already reconciled” question instead of saving; send it again with confirm_checkpoint=1 to save it and break that checkpoint.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
account_id | string | required | The account it happened on. |
direction | string | optional | in or out. Defaults to out. |
amount | number | required | Rupees, always positive. Direction carries the sign. |
date | string | optional | YYYY-MM-DD. Defaults to today. |
category_id | string | optional | One envelope for the whole amount. Money out needs this or the lines below. |
split_category_N, split_amount_N | string | optional | Envelope lines, N from 0. Line 0 carries no amount and takes whatever the others leave. |
payee | string | optional | Free text. Matched against known payees. |
memo | string | optional | A note to your future self. |
tags | string | optional | Comma-separated. |
cleared | boolean | optional | 1 when the bank has confirmed it. |
owner_member_id | string | optional | Who spent it. A card with its own holder still wins. |
confirm_checkpoint | string | optional | 1 answers the “Already reconciled” question. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 200 — The “Already reconciled” question, as HTML. Nothing was saved.
- 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/add \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" \
-d "account_id=acc_hdfc&direction=out&amount=1240" \
-d "date=2026-09-12&category_id=cat_gro&payee=Zepto"
POST /transfer Move between your accounts
A transfer is not spending and is never counted as such. Paying a credit card is a transfer from a budget account to that card. The two accounts must differ.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
from_account_id | string | required | Source account. |
to_account_id | string | required | Destination account. |
amount | number | required | Rupees. |
date | string | optional | YYYY-MM-DD. Defaults to today. |
fee_amount | number | optional | A bank charge on the transfer, in rupees. Recorded as spending, and undone with it. |
fee_category_id | string | optional | The envelope the charge comes out of. Required with fee_amount. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/transfer \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" \
-d "from_account_id=acc_hdfc&to_account_id=acc_card&amount=11600"
POST /transaction/:id/categorise File one transaction
Assign a category to an existing transaction. This is also the learning signal: filing the same payee twice proposes a rule.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
id | path | required | Transaction id. |
category_id | string | optional | The envelope it belongs to. Empty clears it. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/transaction/txn_9a2/categorise \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" \
-d "category_id=cat_gro"
POST /transaction/:id/delete Delete a transaction
Soft delete, recorded in the event log and reversible for thirty days from Activity. Deleting one leg of a transfer deletes both. Refused while a loan instalment rests on it. One dated inside a reconciled period answers with the “Already reconciled” question first.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
id | path | required | Transaction id. |
confirm_checkpoint | string | optional | 1 answers the “Already reconciled” question. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 200 — The “Already reconciled” question, as HTML. Nothing was deleted.
- 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/transaction/txn_9a2/delete \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)"
More transactions routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /add | read | account | The Add form, with an account chosen when given. |
| GET | /transfer | read | from, to, amount | The transfer form, filled in when given. |
| GET | /transaction/:id | read | — | One transaction: its envelope lines, receipts and history. |
| POST | /transaction/:id | read-write | the /add fields, cleared_present, reimbursable_present, confirm_checkpoint | Edit a transaction. A field you do not send keeps its stored value, so date=07-09-2026 alone moves only the date. An empty memo clears it, empty tags removes them, an empty owner_member_id makes it the household's; a blank amount is refused. cleared=1 / reimbursable=1 set a flag and any other value unsets it. Inside a reconciled period it asks first (200). |
| POST | /transaction/:id/settled | read-write | — | Mark a reimbursable transaction as repaid. |
| POST | /transaction/:id/attach | read-write | receipt (a multipart file) | Attach a photo or PDF receipt. |
| GET | /attachment/:id | read | — | A receipt's bytes, with its own content type. |
| POST | /attachment/:id/delete | read-write | — | Remove a receipt. |
| POST | /transaction/:id/convert-to-emi | read-write | tenure_months, annual_rate (required); amount, processing_fee, fee_category_id, name_suffix | Convert a card purchase into an instalment plan. Each instalment's interest carries 18% GST as its own card charge. |
Import, review & rules
Nothing posts itself. Everything arrives in Review first, and rules decide where it goes.
GET /review The review queue
Everything awaiting a decision: imported rows to confirm, suspected duplicates, uncategorised spending, money you are owed, overspent categories, unfunded cards, reconciliations that no longer hold, and proposed rules — only the parts you can see.
Scope
read or higher
Responses
- 200 — The rendered screen, as HTML.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 429 — Rate limit exceeded. Retry after the interval in the response.
Example
curl -sS https://pathayam.yourdomain.com/review \
-H "Authorization: Bearer bgt_…"
POST /import Import a CSV
Paste statement rows as CSV. A saved column mapping whose headers match the file is used on its own; otherwise the columns are guessed, and a guess that works is saved for next time. A file matching neither answers with the “Which column is which?” screen, whose answers go to /import/map. Rows land staged, never posted.
An export from YNAB or Actual Budget is recognised by its header. It covers every account it had there, so the account_id sent here does not apply to it: the answer is the “Which account is which?” screen, as HTML, and that screen posts to /import/budget-app. Nothing is written until then.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
account_id | string | required | Which account these rows belong to. Required, and checked, even for a YNAB or Actual Budget file. |
csv | string | required | The raw CSV text. |
file_name | string | optional | Shown in the import log. Defaults to pasted.csv. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 200 — The column-mapping screen, as HTML, when the file matches nothing known. Nothing was imported.
- 200 — The account-matching screen, as HTML, for a YNAB or Actual Budget export. Nothing was imported.
- 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/import \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" \
--data-urlencode "csv@statement.csv" -d "account_id=acc_hdfc"
POST /import/budget-app Import from YNAB or Actual Budget
The second step of a YNAB or Actual Budget import, the one the account-matching screen from /import posts. Each account in the file is matched to one of yours, or left out. Rows land in Review like any other import, with transfers marked as transfers and split transactions kept split. Transactions only: budget assignments are not imported, and the message says so.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
csv | string | required | The export, as raw CSV text. |
account_0, account_1, … | string | required | One per account in the file, in the order the screen lists them: an account id, or skip to leave it out. |
date_order | string | optional | dd-mm-yyyy or mm-dd-yyyy. Needed only when every date in the file reads both ways round. |
file_name | string | optional | Shown in the import log. Defaults to budget-export.csv. |
Responses
- 303 — Imported. To
/reviewwhen anything waits there, otherwise/import. - 200 — The account-matching screen again, as HTML, when every account was left out. Nothing was imported.
- 400 — Not a YNAB or Actual Budget export, a date that cannot be read, a missing
date_order, or an account with no choice; the message says which. - 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An account id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/import/budget-app \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" \
--data-urlencode "csv@ynab-register.csv" \
-d "account_0=acc_hdfc&account_1=acc_card&account_2=skip"
POST /import/map Say which column is which
Imports a file with the columns you name, and saves the mapping for the next file like it — only if it read at least one transaction. Columns are numbered from 0. Dates are read day first unless you choose otherwise, or the file proves month first; a column that proves both is refused rather than read half wrong.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
account_id | string | required | Which account these rows belong to. |
csv | string | required | The raw CSV text. |
profile_name | string | required | What to call the saved mapping. |
date, narration | integer | required | Their column numbers. |
amount, or debit and credit | integer | required | One signed amount column, or a column each way. |
direction, balance, reference | integer | optional | A Dr / Cr column that signs an unsigned amount, the running balance, the bank reference. |
header_row | integer | optional | The row holding the headers. Defaults to 0. |
date_format | string | optional | How the dates are written: auto (the default), dd-mm-yyyy, mm-dd-yyyy or yyyy-mm-dd. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 400 — A column missing, or dates in both orders; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/import/map \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" \
--data-urlencode "csv@export.csv" -d "account_id=acc_card&profile_name=Card+export" \
-d "date=0&narration=1&amount=3&date_format=mm-dd-yyyy"
POST /import/pdf Upload a statement PDF
Multipart upload of a bank statement. The bank is recognised from its own layout, the file is decrypted in-process if it is password-protected, and the parse is checked against the statement's closing balance. A PDF no reader recognises answers with the column-mapping screen, and one that cannot be opened or read answers with the import screen saying why — both as HTML, with nothing imported.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
statement | file | required | The PDF, as multipart form data. |
account_id | string | required | Which account it belongs to. |
password | string | optional | Omit it to let the app derive it from your saved identity. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 200 — The mapping or import screen, as HTML, when the file could not be imported as it is.
- 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/import/pdf \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" \
-F "statement=@statement.pdf" -F "account_id=acc_hdfc"
POST /import/undo Undo an import batch
Reverses an entire import and every row it created, in one step — except rows edited since, which are left alone and counted in the message. An import that reaches a reconciled period answers with the “Already reconciled” question first.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
batch_id | string | required | From the import log. |
confirm_checkpoint | string | optional | 1 answers the “Already reconciled” question. |
Responses
- 303 — Success. Follow
Locationto the updated screen. - 200 — The “Already reconciled” question, as HTML. Nothing was undone.
- 400 — A field was missing or could not be parsed; the message says which.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 403 — A read-only token.
- 404 — An id that does not exist, or names something you cannot see.
- 409 — The first request with this
Idempotency-Keyis still being processed. - 422 — Understood and refused; the message says why. Also an
Idempotency-Keyreused with a different body.
Example
curl -sS -X POST https://pathayam.yourdomain.com/import/undo \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)" -d "batch_id=imp_41c"
More import, review & rules routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| POST | /review/approve | read-write | staged_id (required); category_id, confirm_checkpoint | Post a staged row, filed into the envelope given. Inside a reconciled period it asks first (200). |
| POST | /review/merge | read-write | staged_id (required); confirm_checkpoint | Merge a staged row into the transaction you already had. Inside a reconciled period it asks first (200). |
| POST | /review/reject | read-write | staged_id (required) | Dismiss a staged row. |
| GET | /import | read | — | The import screen: paste, upload, saved mappings and the import log. |
| POST | /import/profiles/:id/delete | read-write | — | Forget a saved column mapping. |
| GET | /rules | read | — | Rules, and rules the app proposes from what you file. |
| POST | /rules/test | read-write | name, field, op, value, category_id | Show what a draft rule would match among recent transactions. Saves nothing. An unknown field or test is 422. |
| POST | /rules/new | read-write | the same as /rules/test | Save a rule. It applies to anything imported from then on. |
| POST | /rules/:id/apply | read-write | confirm | Without confirm=1, a preview (200) of what it would change in your history; with it, apply it. |
| POST | /rules/confirm/rules/dismiss | read-write | rule_id (required) | Accept or dismiss a proposed rule. |
| POST | /rules/:id/delete | read-write | — | Delete a rule. |
| GET | /payees | read | — | Payees, with how often and how much. |
| POST | /payees/merge | read-write | loser_id, winner_id (required) | Merge one payee into another; every raw string comes across. |
Household & categories
Who has put what toward the shared money, and the envelopes themselves.
More household & categories routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /household | read | month | What each member has committed to the shared budget this month. |
| POST | /household/pick-up | read-write | envelope_id, amount (required); month | Commit more from your own household envelope. Another budget's envelope is 403; a non-positive amount is 422. |
| POST | /household/call-it-even | read-write | envelope_id, amount (required); giving_category_id, note, month | Close part of a balance by agreement; it becomes spending on the giving side. |
| GET | /categories | read | budget | Categories and groups for the budget being viewed. |
| POST | /groups/new | read-write | name (required) | Add a category group to the budget being viewed. |
| POST | /groups/:id/rename/groups/:id/delete | read-write | name (rename) | Rename a group, or remove it. A group must be empty to be deleted. |
| POST | /categories/new | read-write | group_id, name (required) | Add a category. A group the app manages is refused. |
| POST | /categories/:id/rename | read-write | name (required) | Rename a category. |
| POST | /categories/:id/hide | read-write | hidden | hidden=1 hides it, keeping its balance and history; anything else unhides it. |
| POST | /categories/:id/delete | read-write | remap_to | Delete a category, moving its history to remap_to. |
| POST | /categories/:id/merge | read-write | winner_id (required) | Merge into another category: assignments summed month by month, history and targets moved across. Refuses a payment category and a merge across budgets. |
| POST | /categories/:id/target | read-write | amount, kind (monthly or refill), target_date | Set or clear a category's target. A date makes it a by-date target; an empty amount removes it. |
| POST | /categories/:id/reorder/groups/:id/reorder | read-write | direction | Move a category or group up or down. |
Schedules, goals & loans
What is coming, what you are saving toward, and what you owe a lender. The loan routes answer 404 when FEATURE_LOANS is off, and a private loan answers 404 to anyone but its holder.
More schedules, goals & loans routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /schedules | read | days (1 to 3650; default 60), budget | Recurring items and the cashflow calendar. More than 3650 days is 422. |
| GET | /schedules/new | read | — | The new-schedule form. |
| POST | /schedules/new | read-write | name (required); amount, direction, account_id, category_id or envelope lines, recurrence, recurrence_ordinal, recurrence_weekday, next_due, is_subscription | Add a schedule, money in or out. An unknown recurrence is refused, not read as monthly. With monthly-nth-weekday, the ordinal (1 to 4, or −1 for last) and weekday (0 = Sunday) say which day. |
| POST | /schedules/confirm | read-write | name (required); amount (paise), payee_id, account_id, category_id, recurrence, next_due | Turn a detected schedule into a real one. An amount the suggestion could not have made is 422. |
| POST | /schedules/dismiss | read-write | payee_id (required) | Stop suggesting that schedule. |
| POST | /schedules/:id/edit | read-write | the /schedules/new fields | Change a schedule, including its envelope lines. |
| POST | /schedules/:id/delete | read-write | — | Remove a schedule. Everything it already recorded stays. |
| POST | /schedules/:id/paid/schedules/:id/skip | read-write | — | Mark this occurrence paid (or arrived), or skip it. |
| GET | /goals | read | — | Savings goals. |
| POST | /goals/new | read-write | name (required); target_amount, target_date, budget_id | Add a goal. |
| POST | /goals/:id/edit | read-write | name (required); target_amount, target_date | Change a goal. |
| POST | /goals/:id/complete | read-write | resolution (spend, roll or release); with roll: name, target_amount, target_date | Complete a goal, saying what happens to its money. |
| POST | /goals/:id/delete | read-write | — | Delete a goal. |
| GET | /loans | read | — | Loans. |
| GET | /loans/:id | read | — | One loan: schedule, payments, interest. |
| GETPOST | /loans/new | read / read-write | lender, loan_type, annual_rate, tenure_months (required); sanctioned, sanction_date, current_outstanding, history_from, first_instalment_date, interest_model, moratorium_months, nickname, holder_member_id, visibility, disbursement_destination, disbursement_account_id, repayment_account_id | The form, and creating a loan. |
| GETPOST | /loans/what-if | read / read-write | rate, months (required on POST); principal, amount, at_month | Prepayment and rate what-if. Records nothing. |
| POST | /loans/:id/disburse | read-write | amount (required); date, destination, destination_account_id | Record a tranche drawn down. |
| GETPOST | /loans/:id/pay | read / read-write | amount, principal, interest, date, from_account_id | Record an instalment. |
| GETPOST | /loans/:id/prepay | read / read-write | amount, at_month, mode, charge, funding_category_id, preview | Preview or record a prepayment. |
| GETPOST | /loans/:id/rate | read / read-write | effective_from, annual_rate_pct (required on POST); keep, note | Preview both options a rate reset allows, then record one. |
| GETPOST | /loans/:id/statement | read / read-write | lender_outstanding (required); as_of, interest_paid_ytd, instalments_remaining | Record what the lender's statement says, to check the schedule against. A negative figure is 422. |
| POST | /loans/:id/holder | read-write | holder_member_id, visibility | Change whose loan it is and whether it is private. |
| POST | /loans/:id/property | read-write | property_use (self-occupied, let-out, or empty) | What the house the loan paid for is used for. A self-occupied house's accrued interest goes into its holder's tax estimate under 24(b); let-out is shown, not deducted. |
| POST | /loans/:id/settle | read-write | settlement, settlement_account_id, charge, charge_account_id, charge_category_id | Settle a loan early, recording the lender's foreclosure charge. |
| POST | /loans/:id/close | read-write | — | File away a paid-off loan. |
| GET | /loans/:id/schedule.csv | read | — | The amortisation schedule, as CSV. |
Family lending
Money lent to, or borrowed from, family — kept apart from the budget's envelopes. A private arrangement answers 404 to anyone but its holder.
More family lending routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /family | read | — | Everything lent and borrowed. |
| GET | /family/:id | read | confirm (write-off shows the write-off question) | One arrangement and its history. |
| POST | /family/new | read-write | counterparty (required); agreed_total, note, holder_member_id, visibility | Start an arrangement. |
| POST | /family/:id/advance/family/:id/repayment | read-write | amount, account_id (required); date | Record money going out, or coming back. |
| POST | /family/:id/write-off | read-write | category_id (required); confirm | Write off what is left, as spending from an envelope. Asks first without confirm=1. |
| POST | /family/:id/close | read-write | — | Close it, keeping the history. |
Portfolio & net worth
Holdings, hand-valued assets and the net-worth history. Every route here answers 404 when FEATURE_ASSETS is off.
More portfolio & net worth routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /portfolio | read | — | Holdings, XIRR and other assets. |
| GET | /portfolio/:id | read | — | One instrument: lots, prices and gains. |
| GETPOST | /portfolio/add | read / read-write | GET: q, instrument, account. POST: name, account_id (required); instrument_id, scheme_code, symbol, kind, currency, listed, units, unit_price, amount, fees, fx_rate, trade_date, from_account_id, category_id | Search for an instrument and buy it. |
| GETPOST | /portfolio/:id/sell | read / read-write | units (required); price, charges, fx_rate, date, to_account_id | Record a sale, realised first in, first out. |
| GETPOST | /portfolio/cas | read / read-write | statement (multipart PDF), account_id (required); password | Read a CDSL consolidated account statement and show what it found. |
| POST | /portfolio/cas/confirm | read-write | token (required); scheme | Import the holdings from a parsed CAS. |
| GET | /portfolio/allocation | read | — | Allocation by class, region and currency. |
| POST | /portfolio/instrument/:id/classify | read-write | asset_class | Set an instrument's asset class. |
| POST | /portfolio/instrument/:id/listed | read-write | listed, holding_id | Say whether a bond is listed. Listed and held over 12 months is taxed at 12.5%; unlisted or unknown at the slab rate. |
| GETPOST | /portfolio/:id/price | read / read-write | price, as_of | Record a price for one instrument. |
| GETPOST | /portfolio/:id/split | read / read-write | ratio (required); kind, date | Record a split or bonus issue, for every open holding. The same action on the same date is refused a second time. |
| GETPOST | /portfolio/:id/merge | read / read-write | ratio (required); into_instrument_id, date | Record a merger or scheme amalgamation. Cost and purchase dates carry forward. |
| POST | /portfolio/refresh | read-write | — | Fetch fresh prices. |
| GETPOST | /portfolio/asset/new | read / read-write | name, subtype (required); value, as_of, holder_member_id, visibility, paid_from, paid_category | Add a hand-valued asset. |
| GETPOST | /portfolio/asset/:id/add | read / read-write | spent (required); on, paid_from, paid_category, new_value | Buy more of a hand-valued asset: the payment leaving an account and the new value. |
| GETPOST | /portfolio/asset/:id/dispose | read / read-write | proceeds, on, into_account, remaining | Record that an asset is gone, and any money arriving. Values it at zero and closes the account; the history stays. |
| GETPOST | /portfolio/asset/:id/revalue | read / read-write | value (required); as_of | Say what one asset is worth. |
| GETPOST | /portfolio/valuations | read / read-write | value-<id>, asof-<id> per asset | Update every hand-valued asset in one sitting. Blank ones are left alone. |
| GET | /net-worth | read | — | Net worth by group — owed to family and other liabilities included — with a change breakdown that adds up to the total. |
| POST | /net-worth/snapshot | read-write | — | Take a dated snapshot. |
| GET | /fire | read | age, swr, ret, locked, whose | Financial-independence projection from trailing spending. |
Insurance
The policy register: premiums due, paid, lapsed or ended. Only the last four characters of a policy number are kept. A private policy answers 404 to anyone but its holder.
More insurance routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /insurance | read | — | The register. |
| GET | /insurance/:id | read | — | One policy. |
| POST | /insurance/new/insurance/:id/edit | read-write | insurer (required); kind, policy_number, sum_assured, premium, frequency, next_due, start_date, end_date, nominee, covered, tax_group, senior, category_id, holder_member_id, visibility, note | Record or change a policy. |
| POST | /insurance/:id/paid | read-write | back | Move the next due date on by the premium frequency. Posts no transaction. |
| POST | /insurance/:id/status | read-write | status (lapsed, ended or active) | Change a policy's status. A lapsed or ended policy is kept. |
| POST | /insurance/:id/delete | read-write | — | Remove a policy recorded by mistake (undoable from Activity). |
Tax
An income-tax estimate for the signed-in member and one financial year — always your own, never another member's.
More tax routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /tax | read | fy (the year it starts, e.g. 2026), from (policies fills 80C/80D from the insurance register) | The estimate: slab tax, capital gains, 24(b) home-loan interest, TDS and challans netted to a balance or refund, and advance-tax instalments checked against what was paid by each date. |
| POST | /tax | read-write | fy (required); gross, s80c, s80d, s80d_senior, s80d_parents, s80d_parents_senior, s80d_checkup, s80d_parents_checkup, other, hra_received, hra_rent, hra_basic, hra_metro, s24b, tds | Save the declaration. A blank s24b uses the interest accrued on loans marked self-occupied; a figure, even 0, is yours. A year whose rates the app does not have is refused. |
| POST | /tax/payments | read-write | fy, kind (advance or self-assessment), date, amount, note | Record a tax payment of your own. Advance tax must be dated inside its year. |
| POST | /tax/payments/:id/delete | read-write | — | Remove one. Another member's payment is not found. |
Months, reports & query
Closing a month, and reading the past back.
GET /query Filter transactions
The filterable, groupable table. Totals are summed over every row that matched, not the page rendered — the response says how many matched.
Scope
read or higher
Parameters
| Name | Type | Description | |
|---|---|---|---|
period | string | optional | this-month (the default), last-month, last-3, last-12, fy, fy-prev or calendar. |
category | string | optional | Restrict to one envelope, by id. |
account | string | optional | Restrict to one account, by id. |
q | string | optional | Substring match on payee, memo, envelope, tags and the raw imported text. |
scope | string | optional | A budget id, or all (the default). |
group_by | string | optional | category (the default), payee, account, owner, month or tag. |
Responses
- 200 — The rendered screen, as HTML.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 429 — Rate limit exceeded. Retry after the interval in the response.
Example
curl -sS "https://pathayam.yourdomain.com/query?period=last-12&group_by=category" \
-H "Authorization: Bearer bgt_…"
GET /query.csv Query results as CSV
The same filter, as CSV, uncapped. The figures match what the screen showed. A text cell that starts with =, +, -, @, a tab or a carriage return is prefixed with ', so a spreadsheet opens it as text rather than running it; plain numbers are left alone.
Scope
read or higher
Parameters
| Name | Type | Description | |
|---|---|---|---|
… | optional | Accepts every parameter /query does. |
Responses
- 200 —
text/csv. - 401 — No token, or one that is revoked, expired or belongs to a removed member.
Example
curl -sS "https://pathayam.yourdomain.com/query.csv?period=fy" \
-H "Authorization: Bearer bgt_…" -o spend.csv
More months, reports & query routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /search | read | the /query parameters | The same table, titled Search. |
| GET | /months | read | — | Month-close history. |
| GET | /months/:month/close | read | — | The close ritual for one month. |
| POST | /months/:month/close | read-write | note, budget | Close a month. Not a month is 404. |
| POST | /months/:month/reopen | read-write | budget | Reopen a closed month. |
| GET | /reports | read | period (default last-12), scope | Income against spending, trends and loan interest. |
Settings, activity & health
Preferences, the change log and the instance's own health.
GET /activity Every change made
The append-only event log, newest first, with the undo for each entry — the latest hundred you can see. An undo is itself an entry — nothing is edited away.
Scope
read or higher
Parameters
| Name | Type | Description | |
|---|---|---|---|
entity | string | optional | Only changes to one kind of record, such as transaction. |
Responses
- 200 — The rendered screen, as HTML.
- 401 — No token, or one that is revoked, expired or belongs to a removed member.
- 429 — Rate limit exceeded. Retry after the interval in the response.
Example
curl -sS https://pathayam.yourdomain.com/activity \
-H "Authorization: Bearer bgt_…"
POST /activity/:id/undo Undo one event
Reverses a single recorded change within thirty days. Refuses when the record is load-bearing for something later — a loan instalment or a portfolio lot — and says which. When the same record has changed since, it refuses unless you send force=1, which discards those later changes. An undo that would move a reconciled balance answers with the “Already reconciled” question first.
Scope
read-write
Parameters
| Name | Type | Description | |
|---|---|---|---|
id | path | required | Event id from Activity. |
force | string | optional | 1 undoes it even though the record has changed since. |
confirm_checkpoint | string | optional | 1 answers the “Already reconciled” question. |
Responses
- 303 — Undone.
- 200 — The “Already reconciled” question, as HTML. Nothing was undone.
- 404 — No such change, or one you cannot see.
- 422 — Refused — older than the thirty-day window, changed since, or something depends on it; the message says which.
Example
curl -sS -X POST https://pathayam.yourdomain.com/activity/evt_88c/undo \
-H "Authorization: Bearer bgt_…" \
-H "Idempotency-Key: $(uuidgen)"
GET /healthz Health probe
For your uptime monitor. Signed out, it says only whether the instance can serve — nothing about the data. With a token or a session it adds every health check, by name and state.
Scope
public — no token needed
Responses
- 200 —
application/json—{"status":"ok","serving":true,"version":…}. - 503 — Started, but the database does not answer.
Example
curl -sS https://pathayam.yourdomain.com/healthz
More settings, activity & health routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /settings | read | — | Household and personal settings. |
| POST | /settings/theme | read-write | theme (light, dark, system), return_to | Set your theme. Anything else is 400. |
| POST | /settings/theme-toggle | read-write | seen, return_to | Flip between light and dark, returning to a same-site path. |
| POST | /settings/learning | read-write | enabled | enabled=1 suggests rules from what you file; anything else stops new suggestions. Confirmed rules keep working. |
| POST | /settings/overspend-model | read-write | model (reduce-rta or carry-negative) | How cash overspending carries into the next month. Recomputes the history; anything else is 400. |
| POST | /settings/identity | read-write | name (required); pan, dob, mobile, customer_id_axis, customer_id_hdfc, hsbc_card_last6; or clear=1 | Save or clear the details statement passwords are derived from. Never exported or logged. Refused on the demo. |
| POST | /settings/digest | read-write | kind (repeated) | Which digest notices you want; the rest are muted. |
| GET | /health | read | — | The health dashboard, as HTML. |
| POST | /health/backup/health/verify | read-write | — | Run a backup now, or verify that the latest restores. |
| POST | /gmail/fetch | read-write | — | Fetch statements from the linked Gmail now. 403 while viewing as another member; 400 or 502 when Google refuses or cannot be reached. |
| GET | /privacy/terms | public | — | The privacy policy and terms of this instance. |
Exports
Everything back out, in open formats. In every CSV here a text cell that starts with =, +, -, @, a tab or a carriage return is prefixed with ', so a spreadsheet opens it as text; plain numbers are left alone.
GET /export.json Export everything
The complete dataset you can see, in an open format: accounts, transactions, assignments, lots, loans and events. Not a summary — but another member's private accounts and envelopes, and what is recorded in them, are left out. The statement identity and any Gmail refresh token are excluded, and a test asserts their absence.
Scope
read or higher
Responses
- 200 —
application/json, sent as a download.
Example
curl -sS https://pathayam.yourdomain.com/export.json \
-H "Authorization: Bearer bgt_…" -o pathayam-backup.json
More exports routes
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /export.csv | read | — | Every transaction you can see, as CSV. |
| GET | /portfolio/holdings.csv/portfolio/lots.csv/portfolio/prices.csv | read | — | Holdings, first-in-first-out lots, and price history. 404 when FEATURE_ASSETS is off. |
| GET | /net-worth.csv | read | — | The dated net-worth history. |
Session only
These answer 403 to any token, because their path is on the deny-list below. Use them from a browser.
| Method | Path | Token | Parameters | What it does |
|---|---|---|---|---|
| GET | /signin | public | next | The sign-in page. Signed in, it redirects to /. |
| GET | /auth/google/auth/google/callback/auth/oidc/auth/oidc/callback | public | next; code on callbacks | Google and OpenID Connect sign-in. 503 when not configured; 429 after too many attempts. |
| POST | /auth/password | public | email, password, next | Password sign-in. 404 unless LOCAL_LOGIN is set or a password already exists; 401 on a wrong one; 429 after too many attempts. |
| GETPOST | /auth/first-run | public | name, email, password | The first member of an empty household. 404 unless LOCAL_LOGIN is set and no member exists. |
| GETPOST | /auth/setup/:token | public | password, confirm | A one-time password link, opened signed out: choose a first password and be signed in. Works once, for 72 hours. Any unusable link (unknown, used, expired, or no longer allowed) answers the same 410. The POST is rate limited like /auth/password. |
| POST | /auth/dev | public | member_id (required) | Development sign-in. 404 unless DEV_LOGIN is set. |
| POST | /demo/enter | public | member_id | Sign in to the demo as a fictional member, for one day. 404 unless DEMO_MODE is set. |
| POST | /signout | session | — | Sign out. |
| GET | /settings/password | session | — | The password form. |
| POST | /settings/password | session | password, confirm (required); current | Set or change your own password. Changing one needs the current password. |
| POST | /sessions/revoke | session | session_id (required) | Sign one of your devices out. |
| GET | /tokens | session | — | Your API tokens. |
| POST | /tokens | session | name (required); scope, expires_in_days (1 to 3650, or empty for never) | Mint a token, shown once. 403 while viewing as another member. |
| POST | /tokens/:id/revoke | session | — | Revoke a token. It stops working at once. |
| POST | /members/invite | session | email (required) | Let someone in. Refused on the demo. |
| GET | /members/:id/remove | session | — | What removing a member does, and how the balance between you can end. |
| POST | /members/:id/remove | session | resolution | Remove a member. With a balance outstanding, 400 until resolution says how it ends. Clears their password. Refused on the demo. |
| POST | /members/:id/password-link | session | — | Make a one-time link that sets another member’s first password, shown once on the page it returns (200, never a redirect). 422 for yourself, a member with a password, one who has signed in before, or one holding records you cannot see; those come from npm run password-link on the server. Refused on the demo and while viewing as somebody. |
| POST | /impersonate/start/impersonate/writes/impersonate/exit | session | member_id; allow, return_to | View as another member, read-only unless writes are allowed. 404 unless ADMIN_DEBUG is set. |
| GET | /gmail/connect/gmail/callback | session | state, code on the callback | Link a Gmail mailbox. |
| POST | /gmail/disconnect | session | — | Unlink it. |
Writes and the same-origin check
Every write — any method that is not GET, HEAD or OPTIONS — passes one check before it reaches a route, so it covers routes added tomorrow too.
| Rule | What it means |
|---|---|
| Bearer exemption | A request with Authorization: Bearer … and no session cookie skips the whole check. A browser cannot be tricked into sending one on somebody's behalf, so your scripts need nothing extra. A request that also carries a session cookie is held to the rules below, whatever it says in Authorization. |
| The host | The host the request was addressed to must be BASE_URL's hostname, with BASE_URL's port or none — or an IP address or localhost, which no DNS answer can re-point. Anything else is 403: That request was addressed to a different host than BASE_URL. This is what stops DNS rebinding. |
| Behind a proxy | The host is the Host header, or the right-most X-Forwarded-Host entry when TRUST_PROXY is set. A reverse proxy must pass one of them through, or every write from a browser is refused. |
Origin | When present it must be BASE_URL's origin or the origin the request was addressed by. Anything else is 403: That request came from somewhere else. |
Referer | With no Origin, a Referer from one of the same origins will do. Referrer-Policy: same-origin means a same-origin form carries one and a cross-site one does not. With neither header, 403. |
A script in a page of the app that uses the session cookie needs no work either: fetch from that page sends a matching Origin on its own.
What a token cannot reach
A deny-list of path prefixes rather than a note in a review checklist. A token that could mint tokens turns one leak into permanent access, and a token that could change the allow-list turns it into somebody else's household.
| Prefix | Why it is blocked |
|---|---|
/members, /settings/members | The household allow-list: /members/invite and /members/:id/remove. A token must not change who is allowed in. |
/tokens | A token must not mint or revoke tokens, including itself. |
/impersonate | Impersonation is a session-only, logged, human action. |
/auth | Sign-in, SSO, and the development bypass state. |
/signout | There is nothing for a token to sign out of. |
/settings/password | A password is a way in that does not need the token, and would outlive revoking it. |
/sessions | Signing the member's devices out is a human decision. |
/gmail/connect, /gmail/callback, /gmail/disconnect | Linking a mailbox changes what can reach the household. /gmail/fetch stays reachable. |
Leaving should be as easy as arriving
One endpoint returns everything in an open format, and the database is a SQLite file you can open with anything. Nothing about the format needs this app to read it.
Read the guides