01
Start here
- 1
Create a token
In Rudopo, open Settings → API and create a token. Choose its access level (Read, Propose changes or Make changes) and which budgets it reaches. It is shown once — we store only a hash, so it cannot be recovered afterwards.
- 2
Send it as a bearer token
Every request carries Authorization: Bearer rudopo_pat_YOUR_TOKEN. A token acts as you and reaches only what you can, narrowed further to the budgets you chose.
- 3
Ask for a budget
Every budget-scoped resource takes a budget_id. Start with /budgets to find yours; a write names its budget the same way.
Tokens are yours alone. A token reaches only what your account can, can be narrowed to specific budgets, and can be revoked from Settings at any time — revocation takes effect on the next request. Its level and its budgets are fixed once made; for a different level you make a new token.
02
Three ways to connect
One server, one set of rules, three doors in. Whichever you use, a change goes through the ordinary procedure the app itself calls.
A token, for your own scripts
A personal access token from Settings → API, sent as a bearer token to the REST API. A notebook, a spreadsheet, a cron job: anything that speaks HTTP, with no client library to install.
An assistant that signs in
Point an assistant that adds remote MCP servers at Rudopo's address and sign in when it asks. It starts at Read over every space you belong to; to let it propose or make changes, you approve it on a Rudopo screen, never in the chat.
The xAI API, with a token
A model you call yourself over the xAI API can reach the same MCP server with a token in the tool's authorization field. The token's level decides what the model may do.
03
A first request
No SDK to install: it is an HTTP API, and these are whole clients. Each sample has its real answer behind the Response tab, copied from a run against a server built from this code.
Every write below is a dry run
Every write example on this page sends dry_run=true, so pasting one into a terminal changes nothing in a real budget. There is no sandbox space yet; until there is, take the dry run's answer as your first test, then remove the flag and add an Idempotency-Key.
List transactions
Newest first, a page at a time. Money is a decimal string beside its currency; has_more says whether to ask for the next page.
curl -s "https://rudopo.app/api/v1/transactions?budget_id=62a9e36f-6193-4e00-ae2e-ec0105e167e5&limit=2" \
-H "Authorization: Bearer rudopo_pat_YOUR_TOKEN"Add a transaction, with an idempotency key
A dry run first: the answer is the row as the ledger would show it, and nothing is written. Drop dry_run and send an Idempotency-Key to file it — as a proposal under Propose changes, as a row under Make changes.
curl -s -X POST "https://rudopo.app/api/v1/transactions?budget_id=62a9e36f-6193-4e00-ae2e-ec0105e167e5&dry_run=true" \
-H "Authorization: Bearer rudopo_pat_YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"account_id":"13c8f90c-58d5-4f3e-9e3c-2a89df4f9b4f","date":"2026-11-01","amount":"-12.50","payee_name":"Corner Shop","memo":"Milk and bread"}'
# For real: remove dry_run and add a key you mint once per intended change.
# -H "Idempotency-Key: $(uuidgen)"Propose a categorisation
An edit names the row and the version you read it at, so a change the person made in between is a 409, not an overwrite. The dry run shows each field before and after.
curl -s -X PATCH "https://rudopo.app/api/v1/transactions/db6c3d30-9d84-4569-9e28-ec5a65cff890?dry_run=true" \
-H "Authorization: Bearer rudopo_pat_YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"version":0,"category_id":"85c27d89-3a18-4bca-ae3c-a947874c50c1"}'Read the inbox
What your client proposed and what became of it. A dismissed proposal carries the person's reason, so a word there teaches the client. Cursor-paged.
curl -s "https://rudopo.app/api/v1/proposals?budget_id=62a9e36f-6193-4e00-ae2e-ec0105e167e5&status=open" \
-H "Authorization: Bearer rudopo_pat_YOUR_TOKEN"Without dry_run, the same create under Propose changes answers 202 Accepted with a proposal, and nothing has changed yet:
{
"outcome": "proposed",
"proposal": {
"id": "dd258dcd-7f87-48f2-b12d-09d28c7487af",
"status": "open",
"summary": "Add a transaction of -12.50 USD on Nov 1, 2026",
"flags": [],
"diff": {
"type": "create",
"rows": [
{
"date": "2026-11-01",
"accountId": "13c8f90c-58d5-4f3e-9e3c-2a89df4f9b4f",
"accountName": "Arvest Checking",
"labels": [],
"isCleared": false,
"isIncome": false,
"payee": "Corner Shop",
"categoryId": null,
"categoryName": null,
"memo": "Milk and bread",
"amount": {
"amount": "-12.50",
"currency": "USD"
},
"amountNative": null,
"rate": null,
"rateDate": null
}
]
}
}
}04
Read, propose, or write
Every token and every signed-in assistant carries one of three levels, chosen by the person on a Rudopo screen. A client cannot ask its way up.
Read
readSees budgets, accounts and transactions. Changes nothing.
Where a change lands
Nowhere. Every tool and endpoint reads; nothing changes.
Propose changes
proposeEvery change waits in your review inbox until you accept it.
Where a change lands
In the review inbox, as a proposal with a diff. The person accepts, edits or dismisses it; until then nothing has changed.
Make changes
writeChanges land directly. Everything is logged; most can be undone for 15 minutes.
Where a change lands
In the budget directly, logged under the token's name, within the limits set on it. Over a limit it lands in the inbox instead. A delete without the deletes box asks the person in the assistant where it can — a yes deletes — and lands in the inbox when the client cannot ask.
The review inbox
A proposal is a change written down but not made. It is checked when it is filed, against the same rules a real change passes, so a proposal never starts out as something Rudopo would refuse — and checked again when the person opens it, so a row that moved on since shows Superseded instead of being applied. The card shows a diff: a new row as the ledger would show it, each field before and after for an edit, each category's Available before and after for a budget move, the count and the first five rows for a bulk change, with every other row one tap away (Show all) so the person can read each before accepting. The diff your client gets back carries those first five rows. The person accepts it, edits a proposed transaction before accepting, or dismisses it with a one-line reason your client can read back. An open proposal expires after 30 days, and nothing is applied when it does.
The limits a person sets
Four limits on every proposing or writing grant. Empty means the default below, never unlimited.
| Limit | Default | Read on |
|---|---|---|
| Per transaction | 95th percentile of 12 months' outflows; 500 with no history | Read on the absolute converted budget-currency figure of one change. An edit is read at the larger of its before and after. |
| Per day, in total | 10× the per-transaction limit | What a Make changes grant may change directly in one UTC day. Proposals spend nothing from it. |
| Rows per call | 100 | How many rows one request may touch, such as categorizing many transactions at once. |
| Rows per day | 500 | How many rows a Make changes grant may change directly in a day. |
05
Safe by construction
Each of these is a sentence about the code, with the request a skeptic sends to see it hold.
Idempotency keys, everywhere
Every POST, PUT and PATCH takes an Idempotency-Key; DELETE may. The same key with the same body replays the first answer with Idempotency-Replayed: true. The same key with a different body is a 409 idempotency_mismatch. A key whose row has since been deleted is idempotency_gone; a key still running is idempotency_in_progress. A retry never double-writes.
Check it: Send one POST twice with the same key, then once more with a changed amount: 202 or 201, the same again with Idempotency-Replayed, then 409.
Dry run
dry_run=true on any write answers what it would do — the summary, the flags, the diff with every Money figure converted and rounded as the write would store it — and changes nothing. It needs no key and spends none. Every example on this page sends it.
Check it: Send the same body with and without dry_run and compare the Money objects in the two answers: byte for byte the same.
Limits set by the person
Four limits on every proposing or writing grant: an amount per change, an amount per day, rows per call and rows per day. The amount defaults are drawn from the person's own data — the 95th percentile of the last 12 months' outflows in that space, 500 units with no history — and read on the converted budget-currency figure, two-sided, so a large inflow is over the limit too. A trusted write over a limit is filed as a proposal (202, reason ceiling_exceeded), never written and never dropped.
Check it: Set a 50-unit limit and write 60 under Make changes: a 202 with reason ceiling_exceeded, a badged proposal, no row.
The activity log names the client
Every change from a token or an assistant is recorded under the connection's name, and a row it added remembers which connection, so the ledger's Added by filter shows what an app did. The name stays on its proposals and in the log after the token is revoked; the row itself then reads as from A disconnected app. A client with Propose changes or more can read who changed what in a space, without amounts or text.
Check it: Write a row, revoke the token, then filter the ledger by Added by: the row and its history remain.
Undo, for the person and the client
Where a change can be undone the window is about 15 minutes, the same as the person's own undo, and a trusted client may undo only its own changes, inside that window, through rudopo_undo. A new transaction, a new payee or a rename leaves nothing to undo: delete or edit it instead.
Check it: Edit a row under Make changes, call rudopo_activity_list, undo the undo_id it names, then try the same id again: Already undone.
Reconciled rows stay locked
A reconciled transaction's amount, date, direction, account and transfer destination cannot change and it cannot be deleted or uncleared — for a client as for the person, with the same refusal (409, reason reconciled). Its memo, payee and category stay editable.
Check it: Under Make changes, PATCH a reconciled row's amount: 409 reconciled. PATCH its memo: 200 (under Propose changes, a 202 proposal).
One door
A write from the API or an MCP tool is the ordinary procedure the app itself calls: the same schema, the same ownership checks, the same currency conversion, the same locks. The same rules decide on every door, so there is nothing a client can do that Rudopo would refuse the person at the keyboard. The API and the MCP server share one refusal vocabulary; the app says the same thing in its own words. The one money check the API makes on its own is the grid: an amount finer than the currency allows is refused there, where the app would round it.
Check it: Edit a reconciled row's amount in the app, then through the API and an MCP tool: the app refuses it in the row, the API answers 409 reconciled, the tool answers the same sentence with the code.
Never offered, at any level
- Your account, your sign-in, your plan or your other tokens.
- Creating or deleting a space, changing its currency, or sharing it.
- Closing an account, reconciling one, or a balance adjustment.
- A full export. Everything is read one page at a time.
- Moving money. Rudopo does not move money.
06
Connect an assistant
Rudopo runs an MCP server at https://rudopo.app/api/mcp. Any assistant that can reach a remote MCP server with a bearer token can use it with your token; one that signs in with OAuth connects without a token and starts at Read.
curl -s https://api.x.ai/v1/responses \
-H "Authorization: Bearer YOUR_XAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_GROK_MODEL",
"input": "What is my Ready to Assign this month?",
"tools": [{
"type": "mcp",
"server_url": "https://rudopo.app/api/mcp",
"server_label": "rudopo",
"authorization": "rudopo_pat_YOUR_TOKEN"
}]
}'From grok.com, no token needed:
- On grok.com, open Connectors.
- Choose Custom.
- Paste https://rudopo.app/api/mcp as the server URL.
- Sign in to Rudopo when asked, and approve the connection.
- A token's level decides what the tools may do: Read lists the read tools alone; Propose changes and Make changes add the write tools, described for what will happen under that grant. A delete is listed only under Make changes.
- A token, or a connection made by signing in, reaches what a token reaches on the REST API, under the same rate limits. A signed-in connection starts at Read, over every space you belong to.
- Rudopo never sees the conversation. It answers the assistant's requests and logs the changes it asked for; what you said to the assistant stays with the assistant, under its own policies.
07
Things worth knowing first
Money is a string
Amounts are decimal strings in major units with the currency's own decimals ("-45.00" in USD, "1840" in JPY, "1.234" in KWD), never JSON numbers. Parse them with a decimal type — a float will quietly lose cents on someone's ledger. A value finer than its currency allows is a 400, never rounded.
Dates have no timezone
Transaction dates are YYYY-MM-DD and months are YYYY-MM, exactly as entered. They are calendar dates, not instants; converting them to UTC invents a timezone the data never had.
A bad limit is an error, not a trim
Ask for more rows than the maximum and you get a 400. We would rather fail loudly than hand back fewer rows than you asked for and let you ship a partial sync believing it was complete. The same goes for every other parameter: an unrecognised filter value and a date that does not exist are errors, never a request that quietly did something else.
404 never tells you an id is real
An id that does not exist and an id belonging to someone else answer identically, so a token cannot be used to probe for other people's data. A 403 is only ever about your own token — a missing scope, or a budget outside the list you scoped it to.
A proposal is not a row
A 202 with outcome proposed means nothing has changed yet. The row does not exist, the balance has not moved, and a GET will not find it. Read /proposals to see what became of it, and never tell the person it is done.
08
Rate limits
| Scope | Limit |
|---|---|
| Per IP, before authentication | 300 / min |
| Per token, reads | 60 / min |
| Per token, writes | 30 / min |
| Per user, all tokens | 120 / min |
| Per user, daily | 20,000 / day |
| Per user, writes made (burst window, every door) | 60 / 10 s |
| Per user, writes made (hour window, every door) | 1,800 / hour |
| Per user, writes made (day window, every door) | 10,000 / day |
Every response to a request that got past the token check carries X-RateLimit-* and the IETF RateLimit / RateLimit-Policy fields, and a refusal carries Retry-After — so a well-behaved client can pace itself instead of discovering the limit by hitting it. The values describe whichever of the per-token and per-user windows above will refuse first (the per-IP window answers before the token check); a write refused by one of the per-user write windows says its own wait in Retry-After. The write windows count only writes that were allowed, across the web, the phone and every token, so a client retrying on 429 is held to the rate, never locked out for the day.
09
Reference
Every endpoint, parameter and response shape is described in an OpenAPI document generated from the code itself, so it cannot drift from what the server actually does. Every error code has a page of its own, and every tool the MCP server registers is listed from the live catalogue.
There is no official client library, on purpose. The API is small enough to call directly, and publishing the description means you can generate a client in whichever language you actually use rather than waiting for us to write one. Every page here is also served as Markdown at the same address with .md, and /developers/llms.txt links a single-page reference.
10
Changelog
- 2026-11-01The developer pages grow: /developers/mcp renders the live tool catalogue (also as JSON at /developers/mcp.json), /developers/errors explains every code and reason, /developers/changelog is this list on its own page, and every developer page is served as Markdown at the same path with .md. /developers/llms.txt links a single-page Markdown reference. Every write example defaults to dry_run.
- 2026-11-01The next phone app release reviews proposals: Home counts the changes to review and opens Review, where a proposal is accepted, edited or dismissed. Its API tokens screen offers the same three access levels, limits, the deletes box and the budget allowlist as the web.
- 2026-11-01Propose changes and Make changes can now be minted: Settings → API offers the three access levels, the four limits with their defaults, Allow deletes and merges, a budget allowlist, and a 90-day default expiry on a token that changes your budget. An assistant that signed in can be allowed to propose or make changes on the approval page, and Connected apps lists what it may do. The review inbox is live at /review. Nothing changes for an existing token.
New fields and endpoints are added without breaking anything. If an endpoint or a tool name ever has to change in a way that would break you, it gets at least six months of notice and Deprecation headers first.