API & MCP
Galene exposes its data two ways: a JSON REST API (reads for every token; writes opt-in), and an MCP (Model Context Protocol) server that wraps the read API for AI assistants. The MCP server talks stdio when a client launches it, or HTTP on the same host and port as the app at /mcp when an administrator enables it in Settings (default off).
REST API
Section titled “REST API”Nine GET endpoints under /api/v1 stay available to every token: summary, accounts, transactions, categories, tags, budgets, scheduled, cashflow, and notifications. Writes are a separate, opt-in surface described below. All amounts are integer cents (negative = expense); dates are YYYY-MM-DD.
GET /api/v1/accounts returns each account’s opening_balance_cents and opening_as_of (null until set) plus ledger balance_cents computed as opening + transactions on/after as-of (opening is the balance before those transactions). After bank sync it may also include provider_balance_cents and provider_balance_as_of (null for manual accounts or until a sync reports a balance). GET /api/v1/summary balanceCents uses the ledger rule across all accounts.
Tokens are per-user. Create, use, and revoke them on the API tokens page — the token is shown once, at creation.
curl -H "Authorization: Bearer galene_…" http://localhost:3000/api/v1/summaryUnauthenticated requests get a JSON 401.
Write API
Section titled “Write API”The write API is off by default. An administrator turns it on under Settings → API. Until then, POST, PATCH, and DELETE return 403.
A token must be created with write scope. A read token is rejected even when the toggle is on. Browser sessions are not a write credential. Writes cover the data the app already edits: transactions, categories, schedules, and the same accounts, tags, budgets, rules, and splits. There is no separate product object for the API.
Every successful write is stored in an audit log (user, token, action, and the change). The public demo still blocks writes (403), the same as other demo locks. MCP tools are unchanged and stay read-only.
Amounts are integer cents (negative = expense). Dates are YYYY-MM-DD.
curl -X POST -H "Authorization: Bearer galene_…" -H "Content-Type: application/json" \ -d '{"date":"2026-10-04","amount_cents":-500,"account_id":1,"merchant":"Cafe"}' \ http://localhost:3000/api/v1/transactionsPATCH and DELETE use the row id, for example /api/v1/transactions/42. Account delete requires reassign_to. Schedule updates accept edit_scope of once, following, all, or new when changing an existing series.
Webhooks
Section titled “Webhooks”Each user can add more than one webhook. Create and edit use the same form: On, When, Then, and payload fields.
- HTTPS URL. Any other scheme is rejected. There is no unsigned option.
- On. One or more events from the catalog (
transaction.created,category.updated,schedule.deleted, and the same pattern for account, tag, budget, and rule, plussplit.updated). - When. Optional conditions, all of which must match. The fields are the same as auto-categorization rules, plus category: merchant Contains or Equals, amount Equals / More than / Less than / Between (dollars in the form, cents stored), account Is, category Is. An empty When fires for every selected event. There is no OR group — add a second webhook for that. There is no scripting language.
- Then. Galene POSTs the JSON body. You can leave the webhook disabled.
- Payload fields (
event,resource,action,id,name,amount_cents,date,account_id,category_id,merchant,notes,type). Fields you do not select are omitted.
Amount conditions compare the absolute cents value, the same way rules do, so More than 25.00 matches a $40 expense. Merchant Contains is a case-folded substring.
Webhooks saved before this model stored flat filters (account_id, category_id, min_amount_cents, max_amount_cents). Those rows still match with the old signed, inclusive amount range until you save them. Saving rewrites them as conditions.
Galene signs the raw body with the webhook’s secret: header X-Galene-Signature: sha256=<hex>. The secret is shown once when you create the webhook and again when you rotate it. The list and the edit form show only a four-character hint. Webhooks run after a successful change from the write API or from the app (the same save path, once per change).
MCP in the app image
Section titled “MCP in the app image”The app image (:latest / :test; legacy :app-* during cutover) includes the MCP bundle (mcp-bundle.js, ~214 KiB) for stdio clients. There is no separate :mcp-* image. HTTP MCP is served by the app itself at /mcp — no second port.
- Create an API token (Settings → API).
- HTTP (same port as the app): an administrator turns on Enable MCP HTTP under Settings → API (default off). Off means
/mcpis not live. On serves MCP athttp://<host>:<app-port>/mcp. Each client sendsAuthorization: Bearer <token>per request. Do not setGALENE_API_TOKENon the app process for this mode. - Stdio (client-launched): the MCP client starts the bundle itself — no Settings toggle required.
Optional env override: GALENE_ENABLE_MCP=1 / 0 forces MCP HTTP on or off (same pattern as GALENE_OIDC_ENABLED). There is no MCP port or host setting — MCP always uses the app listener when enabled.
Stdio client config
Section titled “Stdio client config”From a source checkout:
{ "mcpServers": { "galene": { "command": "bun", "args": ["/path/to/galene/mcp/index.ts"], "env": { "GALENE_API_URL": "http://localhost:3000", "GALENE_API_TOKEN": "galene_…" } } }}From the app image (stdio session):
docker run --rm -i \ -e GALENE_API_URL=http://host.docker.internal:3000 \ -e GALENE_API_TOKEN=galene_… \ --entrypoint bun \ ghcr.io/galene-finance/galene:latest \ mcp-bundle.js(host.docker.internal is an example for reaching the app from another container on Docker Desktop; use a URL that is reachable from the MCP process.)
HTTP client config
Section titled “HTTP client config”After enabling MCP in Settings:
{ "mcpServers": { "galene": { "url": "http://127.0.0.1:3000/mcp", "headers": { "Authorization": "Bearer galene_…" } } }}Use the same host and port as the app (only port 3000 in compose). Use plain HTTP on localhost or a private network only. Do not expose the app as plain HTTP on a public interface — terminate TLS at the reverse proxy and forward to the app port; /mcp is covered by the same proxy rule as the UI.
Migration from dual-port / :mcp-*
Section titled “Migration from dual-port / :mcp-*”The separate :mcp-* image is no longer published. Operators who ran MCP on a separate port (ADO-35 default 3001) or a :mcp-* container should:
- Pull a current app image (
:latest/:test; replace any:mcp-*compose service). - Enable MCP in Settings → API (or set
GALENE_ENABLE_MCP=1). Default remains off. - Point HTTP clients at
http://<app-host>:<app-port>/mcp(same origin as the UI). No second published port. - For stdio-only clients, launch
mcp-bundle.jsfrom the app image (docker run --entrypoint bun <app-image> mcp-bundle.js) ormcp/index.tsfrom a source checkout. - Remove any
3001:3001/GALENE_MCP_PORT/GALENE_MCP_HOST/:mcp-*compose leftovers.
The MCP server exposes: galene_summary, galene_accounts, galene_transactions, galene_budgets, galene_scheduled, galene_categories, galene_notifications, and galene_version. Tools stay read-only. A missing or unknown token is rejected; delete the token in Settings → API and the next request from that client fails.