Kura

API and MCP

Kura has a REST API and an MCP server, both reading and writing the same vault you see in the app. They exist so your own scripts and spreadsheets can reach your records, and so an AI assistant can answer questions about them — with your permission, which you grant and withdraw yourself.

Amounts are integers

Every amount is an integer in the minor unit of its own currency. 1299 means €12.99. 1299 also means ¥1,299, because the yen has no minor unit.

So do not divide by 100. Divide by the exponent of the currency named on the record. A client that assumes hundredths everywhere is wrong about the yen and right nowhere it noticed — and nothing in a response will look wrong while it happens.

The same convention holds on the way in. To record €25.00 you send 2500. Kura never accepts or returns a decimal amount anywhere, because a float that has passed through JSON is no longer the number that was written down.

Dates are plain YYYY-MM-DD strings and never timestamps, for a related reason: an instant shifts across midnight depending on where it is read, and lands a purchase in the wrong month.

Getting a token

In the app, go to Settings → API tokens. Name the token after whatever will hold it, choose whether it may only read or also write, and copy the value — it is shown once and cannot be recovered, because only a hash of it is stored. A vault may hold 10 of them at a time.

A token is a password to your entire financial history. It does not expire, there is no second factor, and anyone holding the string can do whatever its scopes allow. Give each one only the access it needs, keep it out of anything shared or committed, and revoke it the moment it stops being used.

The two scopes are read and write. Write implies read. Nothing in either scope can delete your account, change your password, or create another token — those need the session in a browser.

Authenticating a request

Send the token as a bearer credential. There is no other accepted form.

request

curl https://kura-tau.vercel.app/api/v1/accounts \
  -H "Authorization: Bearer kura_<id>_<secret>"

There is no uid parameter anywhere in this API. Which vault you are reading is determined by the token and nothing else, so a request cannot name somebody else’s data even by accident.

300 requests per hour per token, shared between the REST API and MCP. Past that you get 429 with {"error":"rate_limited"}. A rejected credential does not spend the budget, so a bad token cannot be used to lock out a good one.

Failures are JSON with a stable error string:

  • 401 unauthorized— missing, malformed or unrecognised token. “No such token” and “wrong secret” are deliberately indistinguishable.
  • 401 expired_token — an OAuth access token past its lifetime. Refresh and retry rather than asking the user again.
  • 403 insufficient_scope — a valid token without the scope for this call. Presenting it again will not help.
  • 400 invalid_body — with an issues array naming each field and what was wrong with it.
  • 404 not_found, 429 rate_limited, 500 internal_error.

Endpoints

Base URL https://kura-tau.vercel.app/api/v1. Start at the index: it reports your base currency, which every converted figure is expressed in.

  • GET/api/v1

    Base currency, your token’s scopes, the rate limit, and the amount convention.

  • GET/api/v1/accounts

    Every account with its balance, archived ones included.

  • GET/api/v1/records?from=&to=&limit=&cursor=

    Records newest first. Default 100, maximum 500. Dates are inclusive.

  • GET/api/v1/records/{id}

    One record.

  • POST/api/v1/records

    Record one expense or income. Needs the write scope.

  • PATCH/api/v1/records/{id}

    Replace a record whole. Needs the write scope.

  • DELETE/api/v1/records/{id}

    Remove a record and undo its effect on the balance.

  • GET/api/v1/categories

    The fixed category vocabulary, for both directions.

  • GET/api/v1/labels

    Labels, archived ones included.

  • GET/api/v1/people

    The household profiles.

  • GET/api/v1/budgets

    Monthly budgets.

  • GET/api/v1/goals

    Savings goals.

  • GET/api/v1/recurring

    Recurring rules.

Only /records pages, because it is the only collection without a bound. Its cursor is opaque — pass back whatever nextCursor gave you. Its absence means the last page. The cursor is stable across writes, so walking a decade will not skip or repeat a row because something was added while you read.

Writing records

POST /api/v1/records

{
  "kind": "expense",
  "accountId": "miUlcGTTxR2qoeKGUe1N",
  "amount": 2500,
  "category": "groceries",
  "date": "2026-08-14",
  "note": "Weekly shop",
  "personId": "7VonRfp2p6waIPhTH6w7",
  "shared": true,
  "labelIds": ["lbl-japan"]
}

The first five fields are required; the rest are optional. Categories come from /api/v1/categories and cannot be invented — a closed vocabulary is what keeps a report comparable across years.

Some fields you cannot set, and Kura ignores rather than rejects them. currency is always the account’s currency; baseAmount and fxRate are computed and then frozen, so a past record never changes value when rates move. The response is the stored record read back, not an echo of your request, so you can see what was actually written — including the case where no exchange rate was available and the record went in without a base value.

PATCH replaces the whole record rather than merging. Send every field you want to keep: an omitted note clears the note, and an omitted amount is rejected rather than being read as zero. The balance arithmetic reverses what is stored and applies what you send, so a partial body would move the balance by the difference between your request and reality.

Connecting an AI assistant

Kura is an MCP server at /api/mcp, speaking Streamable HTTP, protocol revision 2025-06-18. There are two ways in.

Hosted connectors— Claude and ChatGPT’s connector panels. Add a custom connector and give it the server URL. It will discover the rest, send you to a Kura page to approve the access, and handle the tokens itself.

server url

https://kura-tau.vercel.app/api/mcp

Desktop and editor clients — Claude Desktop, Claude Code, Cursor, Windsurf. These reach a remote server through the mcp-remote bridge, with a token from Settings.

claude_desktop_config.json

{
  "mcpServers": {
    "kura": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://kura-tau.vercel.app/api/mcp",
        "--header", "Authorization: Bearer kura_<id>_<secret>"
      ]
    }
  }
}

Either way, what the assistant may do is what the token or the approval allows. A read-only connection is not shown the tools that write, so it cannot try them and then tell you your vault is broken. You can end any connection under Settings → API tokens: approved applications are listed separately from tokens you made by hand, and disconnecting one revokes everything it holds at once.

What an assistant can do

  • readget_overview

    Accounts and balances

  • readsearch_records

    Find records

  • readspending_report

    What a period cost

  • readlabel_report

    What each label cost

  • readsuggest_recurring

    Payments that look like they repeat

  • writecreate_record

    Record one expense or income

The reporting tools are the same code the app’s own screens are drawn from, which matters for one specific reason: they refuse rather than guess. Ask for a period too large to total exactly and the answer is an explicit refusal with the reason, not a figure that happens to be short. A savings rate with no income is null, not zero. Records whose value in your base currency is unknown are reported as left out rather than counted as nothing.

That is worth more to a language model than to a person. Somebody who meets a blank cell asks why; a model handed a plausible number tends to narrate it in a sentence indistinguishable from the true ones around it. Every refusal therefore carries an instruction not to estimate — so if an assistant tells you a figure could not be computed, that is Kura declining to invent one.

Writing is deliberately narrow: one record per call, and the tool says in its own description that it moves your balances. There is no bulk recategorise.

OAuth, for client authors

Standard authorization-code flow with PKCE, discoverable from the two metadata documents. Dynamic client registration is open, so no key needs arranging with us.

  • GET/.well-known/oauth-authorization-server

    RFC 8414. Endpoints and what is supported.

  • GET/.well-known/oauth-protected-resource

    RFC 9728. Which authorization server guards /api/mcp.

  • POST/api/oauth/register

    RFC 7591. Public clients only; no secret is issued.

  • GET/oauth/authorize

    The consent screen. Requires the user to be signed in.

  • POST/api/oauth/token

    authorization_code and refresh_token grants.

Three things will fail a client that has cut a corner elsewhere:

  • PKCE is required and only S256 is accepted. A request without a challenge is refused rather than treated as a flow without one.
  • Redirect URIs must match exactly what you registered — every character, including the trailing slash and the query string. No prefix matching.
  • Codes are single-use and short-lived, and a failed exchange burns the code. Start a new authorization rather than retrying.

Access tokens last 1 hour. Refresh tokens are rotated on every use, so store the new one each time; the old one stops working immediately. A refresh may narrow its scope but never widen it — asking for more than the user approved returns what they approved.

What this API does not do

Some of these are omissions and some are refusals. It is worth knowing which.

  • No batch writes. One record per request. Firestore has no transaction spanning many independent account updates, so a partly-applied batch would move some balances and not others — and report it in a status code nobody checks.
  • No transfers. Money moving between two of your own accounts touches two balances and is neither spending nor income. Record those in the app, where both sides are visible.
  • No bulk recategorisation. It is the operation most likely to rewrite months of history from one mistaken instruction, and to do it invisibly.
  • No custom categories. The vocabulary is fixed so that a report means the same thing in March as in November.
  • No writes to accounts, budgets, goals, labels, people or recurring rules. Readable, not writable. Ask if you need one of these.
  • No webhooks and no bank connections. Kura never holds banking credentials, and that is not a gap waiting to be filled.

What this means for your privacy

Everything else in Kura is built so that your records go nowhere but your own browser and your own vault. A token changes that, by your choice: whatever holds it can read your financial history, and if that is an AI assistant then your records reach whichever company runs the model — not Kura.

We cannot see what you send through a connection you authorise, and we do not receive a copy. But we also cannot take it back for you. That is the trade a token makes, and it is why nothing here is on by default, why each token states what it may do, and why every connection can be ended from Settings.

This reference is written in English only. Kura’s interface is available in several languages; this page is not, because a mistranslated instruction about somebody’s money is worse than an untranslated one.