Contents
GirovoAPI
With Girovo API your app works with Girovo on the same computer. It reads accounts and bookings, changes categories, notes, budgets and settings, and proposes transfers. Every app asks the user first.
- For
- Girovo Next 7.0, Windows and Linux
- Connection
- only on this computer
- Default
- off
- Protocol
- JSON-RPC 2.0, one message per line
Girovo API belongs to Girovo Next, the preview of Girovo 7. Methods and answers may still change until version 7.0.
The essentials
- Girovo API is off until the user switches it on: Einstellungen › Girovo API › Girovo API zulassen.
- There is no network and no HTTP. On Windows it is a named pipe, on Linux a Unix socket. Only the signed in user can reach it.
- Every app asks first. Girovo shows a dialog with the app's name, its publisher and each permission on its own.
- Girovo reads the publisher from your app's code signature. What the app says about itself does not count for this. Unsigned apps show as "Herausgeber unbekannt" (publisher unknown).
- Your app only proposes transfers. Girovo opens them filled in, the user checks and sends them, then the bank asks for its approval.
- The user can take back any permission in Einstellungen › Girovo API, for each app on its own.
Connecting
Open the pipe or the socket and send JSON-RPC 2.0, one message per line, ending in \n. Answers and events come back the same way.
Address
| Windows | \\.\pipe\girovo-api-<SID>The user's SID, for example from whoami /user. |
|---|---|
| Linux | $XDG_RUNTIME_DIR/girovo-api.sockWithout XDG_RUNTIME_DIR the socket is in ~/.config/Girovo. |
The first call: girovo.hello
Every connection starts with girovo.hello. Before it, Girovo answers nothing else. You give your app's name, the permissions you need and a short reason for the user.
{
"jsonrpc": "2.0",
"id": 1,
"method": "girovo.hello",
"params": {
"app": "My Household Book",
"version": "1.0",
"permissions": [
"accounts.read",
"transactions.read"
],
"reason": "Shows your balances in the household book."
}
}The answer comes once the user has decided. granted lists every permission your app has now. The user can switch single permissions off, so check granted.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"granted": [
"accounts.read",
"transactions.read"
],
"girovo": {
"version": "7.0.0",
"api": 1
}
}
}Wrapped here for reading. On the wire every message is a single line.
What the dialog shows
- Your app's icon and name, from its executable, not from hello.
- "Herausgeber: <name>" with a check mark when Windows trusts the signature. Otherwise "Herausgeber: unbekannt" as a warning, with the file's path.
- Each permission as its own switch. The user can turn single ones off.
- Your reason from hello, marked as the app's words.
- Zulassen (allow) and Ablehnen (refuse).
What can happen
| Allowed | Girovo remembers it until the user takes it back. A later hello asks only for permissions that are missing. |
|---|---|
| Refused | refused_by_user. After that your app gets denied until Girovo restarts. |
| No answer | timeout after 2 minutes. This is not a refusal, you may ask again. |
| Not signed in | The call waits. Girovo comes to the front and says at the login which app asks. After 2 minutes without a login you get locked. |
| Too much at once | busy, for example with more than 4 connections from one program. |
Permissions
Ask only for what your app really needs. If a permission is missing later, you get not_permitted with data.permission. A new hello with that permission opens the dialog just for it.
| Permission | Allows |
|---|---|
accounts.read | Read accounts, balances, available amounts and the total balance |
transactions.read | Read and search bookings with notes and categories |
insights.read | Read the Analyse figures, contracts and subscriptions, budgets and savings goals |
ask | Ask Girovo AI (no method yet) |
categories.write | Categories of bookings, rules and own categories |
notes.write | Notes on bookings |
budgets.write | Monthly budgets, savings goals, reminders and contract names |
settings.read | Read settings |
settings.write | Change settings, only the allowed ones |
bank.refresh | Fetch new data from the bank. If the bank asks for an approval, the user gives it in Girovo. |
transfers.request | Propose transfers and scheduled transfers. The user checks and sends each one in Girovo. |
Methods
All methods start with girovo. Amounts are whole cents with a currency, such as {"cents":1299,"currency":"EUR"}. Days are yyyy-mm-dd, months yyyy-mm. Accounts, bookings and contracts have their own IDs from the read methods.
Reading
- girovo.accounts.listPermission: accounts.read
{}{ accounts[], total[] }: each account with id, name, type, iban, bank, balance and available, plus the total balance per currency.
- girovo.transactions.listPermission: transactions.read
{ account?, from?, to?, query?, category?, limit?, cursor? }{ transactions[], next? }: bookings, newest first. limit 1 to 500, default 100. next is the cursor for the next page.
- girovo.insights.monthPermission: insights.read
{ month? }Income, spending, net and both by category for one month, by default this month. coverage says whether the month is fully loaded.
- girovo.contracts.listPermission: insights.read
{}{ contracts[] }: contracts and subscriptions with the amount per month and year, last and next day.
- girovo.budgets.listPermission: insights.read
{ month? }{ month, coverage, budgets[] }: each monthly budget with spending, rest and state (left, exact, over, unknown).
- girovo.goals.listPermission: insights.read
{}{ goals[] }: savings goals with target, balance and what is still missing.
Changing
- girovo.categories.setPermission: categories.write
{ booking, category | null }{ booking, category }. null puts the booking back to its rule or the suggestion.
- girovo.rules.addPermission: categories.write
{ booking, category } | { payee, category }{ applied, payees[], … }. If the user filed the payee by hand, that stays: applied false, reason filed_by_hand.
- girovo.categories.createPermission: categories.write
{ name, direction?: "out" | "in" }{ category: { id, name }, direction }
- girovo.notes.setPermission: notes.write
{ booking, text | null }{ booking, note }. One line. null or empty removes the note.
- girovo.budgets.setPermission: budgets.write
{ category, cents | null }{ category, cents }. Spending categories only. null removes the budget.
- girovo.goals.setPermission: budgets.write
{ account, name, target | null, by? }{ account, goal }. Savings accounts only, by as yyyy-mm. Where savings goals need Girovo Plus you get not_permitted.
- girovo.reminders.setPermission: budgets.write
{ contract, by | null, lead?: 7 | 14 | 30 }{ contract, reminder }. A reminder for a contract, lead days before.
- girovo.contracts.renamePermission: budgets.write
{ contract, name | null }{ contract, name, own }. null brings back the detected name.
Settings
- girovo.settings.getPermission: settings.read
{}{ settings, onlyOn[], onlyOff[] }: the settings your app can read and change. onlyOn and onlyOff name the ones that only go one way.
- girovo.settings.setPermission: settings.write
{ key, value }{ key, value, changed }. Other keys give invalid.
Allowed keys: language, theme, blackBackground, idleMinutes (5, 10, 15, 30), balancesAtLogin, bookingsAtLogin, keepBalances and keepBookings (on only), companyLogos (off only), ai, aiTone, aiLength, aiUseName, aiNames, singleKeyShortcuts, accountOrder, accountGrouping (type, bank, none), hiddenAccounts.
Bank and transfers
- girovo.bank.refreshPermission: bank.refresh
{ account? }{ accounts: [{ account, state, newBookings?, balance? }], newBookings }. Fetches new data as Girovo itself does. The user gives any approval in Girovo. Can take up to 10 minutes.
- girovo.transfers.requestPermission: transfers.request
{ iban, name, cents, purpose?, executeOn?, account? }{ request, state: "pending" }. Girovo opens the transfer filled in, marked as asked by your app. With executeOn it becomes a scheduled transfer where the account offers them.
- girovo.transfers.statusPermission: transfers.request
{ request }{ request, state, text?, changed? }. Your own requests only.
state is pending until the user acts. Then sent (the bank took it), refused (closed without sending), failed (the bank refused it, its words in text) or unclear (not yet known whether it went through). changed: true means the user sent other values. Girovo does not say which. The result also comes as the event girovo.transfer.updated on the connection that asked.
Events
- girovo.events.subscribePermission: per kind
{ kinds[] }{ subscribed[] }
- girovo.events.unsubscribePermission: none
{ kinds[]? }{ subscribed[] }. Without kinds every subscription of this connection ends.
Errors
An error is a JSON-RPC error. error.data.reason is the code from this list, and error.data.detail the finer reason where there is one, such as never for a setting no app may change. Fields such as permission, param or text come along. The message is meant for your log, not for your interface.
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32003,
"message": "This app does not have the permission for this method.",
"data": {
"reason": "not_permitted",
"permission": "insights.read"
}
}
}| reason | code | Meaning |
|---|---|---|
locked | -32001 | Girovo is locked or nobody is signed in. Try again later. |
denied | -32002 | The user refused your app. Until Girovo restarts this holds for everything. |
not_permitted | -32003 | Your app lacks a permission, data.permission names it. Without data.permission, message gives the reason, such as too many open transfer requests. |
refused_by_user | -32004 | The user refused in the dialog or at the approval. |
bank_error | -32005 | The bank reported an error, its words in data.text. |
timeout | -32006 | No answer in time, for example to the dialog. You may ask again. |
rate_limited | -32007 | Too many calls in a short time. |
too_large | -32008 | The message is larger than 1 MB. |
busy | -32009 | Too many connections or waiting calls. |
invalid | -32602 | A parameter is wrong, data.param names it. Also any call before hello. |
method_not_found | -32601 | This method does not exist. |
internal | -32603 | Girovo could not answer the call. |
Events
With girovo.events.subscribe your connection listens for events. They come as JSON-RPC notifications without an id, with the method girovo.<kind>. Each kind needs a permission. If the user takes it back, its events stop at once. A subscription holds only for this connection.
→ {"jsonrpc":"2.0","id":3,"method":"girovo.events.subscribe","params":{"kinds":["bookings.new","balances.changed"]}}
← {"jsonrpc":"2.0","id":3,"result":{"subscribed":["bookings.new","balances.changed"]}}
← {"jsonrpc":"2.0","method":"girovo.balances.changed","params":{"account":"a_…","balance":{…},"available":{…}}}- bookings.newPermission: transactions.read
{ account, count, bookings[] }New bookings on an account that was already loaded.
- balances.changedPermission: accounts.read
{ account, balance, available }A balance changed.
- transfer.updatedPermission: transfers.request
{ request, state, text?, changed? }A transfer request from your app is settled.
Example in Node.js
Connect, hello, read the accounts. No package needed, Node 18 or newer will do. Girovo must be running with Girovo API switched on.
// girovo-client.mjs: node girovo-client.mjs
import net from 'node:net'
import { execFileSync } from 'node:child_process'
function address() {
if (process.platform === 'win32') {
// "PC\\name","S-1-5-21-…"
const line = execFileSync('whoami', ['/user', '/fo', 'csv', '/nh'], { encoding: 'utf8' })
const sid = line.trim().split(',').pop().replaceAll('"', '')
return '\\\\.\\pipe\\girovo-api-' + sid
}
return process.env.XDG_RUNTIME_DIR + '/girovo-api.sock'
}
const socket = net.connect(address())
socket.setEncoding('utf8')
const waiting = new Map()
let nextId = 1
let buffer = ''
function call(method, params = {}) {
const id = nextId++
socket.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n')
return new Promise((resolve, reject) => waiting.set(id, { resolve, reject }))
}
socket.on('data', (chunk) => {
buffer += chunk
let at
while ((at = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, at)
buffer = buffer.slice(at + 1)
if (!line.trim()) continue
const message = JSON.parse(line)
if (!('id' in message)) continue // an event
const { resolve, reject } = waiting.get(message.id)
waiting.delete(message.id)
if (message.error) reject(message.error)
else resolve(message.result)
}
})
socket.on('connect', async () => {
try {
const hello = await call('girovo.hello', {
app: 'My Household Book',
version: '1.0',
permissions: ['accounts.read'],
reason: 'Shows your balances in the household book.',
})
console.log('Allowed:', hello.granted)
const { accounts } = await call('girovo.accounts.list')
for (const a of accounts) console.log(a.name, a.balance?.cents, a.balance?.currency)
} catch (error) {
console.error(error.data?.reason, error.message)
} finally {
socket.end()
}
})Girovo MCP
Girovo MCP connects AI assistants such as Claude Desktop or Codex to Girovo. It is an MCP server over stdio that uses Girovo API. The assistant gets the same dialogs and permissions as any other app.
Where it is
| Windows | %LOCALAPPDATA%\Programs\Girovo\girovo-mcp.exe |
|---|---|
| Linux (deb) | /usr/bin/girovo-mcp |
| Linux (AppImage) | ~/.config/Girovo/girovo-mcp |
Claude Desktop
Add Girovo MCP to claude_desktop_config.json. On Windows the file is in %APPDATA%\Claude. Write out the full path and double every backslash in JSON.
{
"mcpServers": {
"girovo": {
"command": "C:\\Users\\DEIN-NAME\\AppData\\Local\\Programs\\Girovo\\girovo-mcp.exe"
}
}
}On Linux, command is the full path, for example "/usr/bin/girovo-mcp". Restart the assistant afterwards.
Other MCP clients
The same goes for Codex and other clients: the command is the path to girovo-mcp, with no arguments.
How it behaves
- With the first tool, Girovo MCP asks for accounts.read, transactions.read and insights.read, plus that tool's permission. So one dialog covers the start.
- If a later tool needs another permission, Girovo opens the dialog just for that one.
- On Windows the dialog shows the assistant, for example "Claude (Anthropic PBC) über Girovo MCP". On Linux it shows Girovo MCP with its path.
- If Girovo is closed or Girovo API is off, the tool says so. The next call connects again.
The 19 tools
| Tool | Method | Permission |
|---|---|---|
list_accounts | accounts.list | accounts.read |
search_transactions | transactions.list | transactions.read |
month_summary | insights.month | insights.read |
list_contracts | contracts.list | insights.read |
list_budgets | budgets.list | insights.read |
list_savings_goals | goals.list | insights.read |
set_booking_category | categories.set | categories.write |
add_category_rule | rules.add | categories.write |
create_category | categories.create | categories.write |
set_note | notes.set | notes.write |
set_budget | budgets.set | budgets.write |
set_savings_goal | goals.set | budgets.write |
set_reminder | reminders.set | budgets.write |
rename_contract | contracts.rename | budgets.write |
get_settings | settings.get | settings.read |
set_setting | settings.set | settings.write |
refresh_from_bank | bank.refresh | bank.refresh |
request_transfer | transfers.request | transfers.request |
transfer_status | transfers.status | transfers.request |
If your assistant runs in the cloud, everything it reads from Girovo goes to its provider.
Limits
What Girovo allows
| Connections per program | 4 (16 in all) |
|---|---|
| First call after connecting | within 30 s |
| Calls per second | 20, briefly up to 40 |
| Open calls per connection | 32 |
| Size of a message | 1 MB |
| Answer to a call | up to 150 s |
| girovo.bank.refresh | up to 10 minutes |
| Waiting for the login | 2 minutes, 64 calls at most |
| Open transfer requests | 3 per app, 10 in all |
What no app can do
- The PIN, Windows Hello and security keys
- "Weitere Banken mit anmelden" and forgetting the device
- Deleting or resetting saved data, restoring a backup
- Girovo API itself: the switch and the permissions, its own included
- The update channel and updates
- Usage data, "Neue Umsätze melden" and the sync
- Sending a transfer: the user always does that in Girovo
Girovo is source available under the PolyForm Perimeter License 1.0.1. You can find the source code on GitHub.