Zeno
Server Details
AI bookkeeper for Swiss small companies: books invoices, reconciles the bank, keeps VAT current.
- Status
- Healthy
- Last Tested
- Transport
- Streamable HTTP · MCP 2025-11-25
- URL
TDQS
Score is being calculated.
Available Tools
51 toolsaccept_autopost_decisionInspect
One-click accept of a held decision (accountant) — posts the AI's proposal.
The accepting actor's approval replaces every soft guardrail (confidence,
amount cap, risk flags); hard blocks stay enforced by the posting path (closed
period / sealed FY / unresolvable accounts → 409/422). The accepted entry posts
as origin=ai with the accepting user in the audit trail
(accepted_by/accepted_at + JE posted_by); the row flips
held → posted in place, so a second accept is a 409 (idempotent).
accepted_by_kind records what kind of actor approved: an accept made over
MCP is recorded as mcp, not as a person's click.
The optional body serves three holds and is omitted for every other kind:
expense:receipt:item: needs employee_id + category_id for the draft
claim, autopost:capitalize: needs fixed_asset_category_id (+ optional
useful_life_months) when the hold resolved no category, and a
needs_split_source mixed invoice needs reclassify_from_account_code —
422 without them.
| Name | Required | Description | Default |
|---|---|---|---|
| body | No | ||
| company_id | Yes | ||
| decision_id | Yes |
call_operationInspect
Run any operation by name — the ones in your tool list and the unlisted ones alike.
name is an operation name from find_operations; arguments is the
object described by describe_operations (omit it for an operation that
takes none). The result, and any error, are exactly what calling the
operation directly would give you.
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | ||
| arguments | No |
collect_api_keyInspect
Collect the key once the user has approved.
status is one of:
pending— nobody has approved yet, or (withthrottled: true) we were rate-limited and learned nothing. Waitpoll_interval_seconds, then call again — every answer carries it. Do not ask the user to tell you when they are done: you cannot detect it any other way, and handing the wait back to the user is how these sessions die.ready—api_keyis the credential. Follow thenotethat comes with it before anything else: the key does nothing until the client's configuration holds it, and calling again issues nothing.scopesays what it can reach (onboarding,companyorfull) andcompany_idwhich company, if any. Whenscopeisonboardingthe user has no company yet and this key exists to make one: callonboarding_guidenext — it needs no key, so read it while the user installs this one. It is the only place that says which documents to ask them for, and asking for them all at once is the difference between one conversation and seven.collected— this request already issued its key and nothing new was issued. Follow thenote.denied— stop. Do not start another request; a human refused it ("that was not me"). Raising a second one is how a refusal turns into a prompt the user has to refuse repeatedly.expired— the request timed out. Do not simply open another one, and what to do depends on which door you opened, which only you know.request_api_key: ask whether they saw the approval page, and call it again if they want to retry.start_signup: they may have finished registering in the browser meanwhile, in which case they now HAVE an account andrequest_api_keyis the path — ask. If they did not, tell them plainly that it timed out; starting another signup without asking is how a refusal turns into a prompt they have to refuse repeatedly.
A pending answer is a success, not an error; it is deliberately not an error
status, because an error would be read as "do not retry" and retrying is the
entire protocol. A rate limit mid-wait is reported the same way, with
throttled: true and a longer poll_interval_seconds — slow down, do not stop.
| Name | Required | Description | Default |
|---|---|---|---|
| claim_token | Yes |
create_ar_invoiceInspect
Create an outgoing (sales) invoice as a draft — nothing is booked yet (accountant role).
A draft changes no ledger balance, gets no invoice number and appears in no VAT
return; it exists so it can be edited. Posting happens later, at
POST …/ar/invoices/{invoice_id}/issue.
Body: issue_date (required), counterparty_id (the customer — required
before it can be issued), currency/language, due_date (filled from the
customer's payment terms when omitted), doc_type (invoice, the default — a
credit_note draft can never be issued: credit an issued invoice with
POST …/ar/invoices/{invoice_id}/credit-note instead), and lines: each line
carries quantity, unit_price, a revenue account_code and a
vat_code_id (the id from GET …/gl/vat-codes; a line without one declares no
VAT). Totals, per-line net/VAT/gross and discounts are computed server-side — do
not send them.
Returns the created invoice with its lines and computed totals. Check
GET …/ar/invoices/{invoice_id}/readiness for what still blocks issuing.
| Name | Required | Description | Default |
|---|---|---|---|
| lines | No | ||
| notes | No | ||
| currency | No | CHF | |
| doc_type | No | invoice | |
| due_date | No | ||
| language | No | ||
| company_id | Yes | ||
| issue_date | Yes | ||
| legal_text | No | ||
| footer_text | No | ||
| counterparty_id | No | ||
| payment_terms_id | No | ||
| issuer_profile_id | No | ||
| service_period_end | No | ||
| credited_invoice_id | No | ||
| service_period_start | No | ||
| invoice_discount_kind | No | none | |
| invoice_discount_value | No | 0 |
create_gl_journal_entryInspect
Create a draft journal entry by hand — accountant. It carries NO source document unless you name one.
Only the pipeline paths stamp source_ref_type/source_ref_id (an ingested
document is 'item'), so by default a booking made here cannot be traced back to
what justified it. document_id is the retrofit: name an existing gl_document
(create one with POST …/gl/documents) and it is attached as the entry's source
document, so the entry is not evidence-less. The same attach is available afterwards
at POST …/gl/journal-entries/{id}/documents — including on a posted entry.
Use it for bookings with genuinely no document behind them — reclassifications, provisions, accruals, year-end adjustments. Do not transcribe a document into it and then attach the file — that files the evidence beside a booking nobody derived from it. Each of these has a door that keeps the evidence attached:
an invoice, receipt or statement → ingest the file (
POST …/uploads/or…/intake), then accept what the coder proposes (…/autopost-decisions);an already-ingested item →
POST …/gl/items/{item_id}/coding;a bank movement with no document →
POST /bank-entries/{entry_id}/manual-coding;opening balances →
POST …/opening-entriesand post the proposal.
auto_reverse_date arms the entry as a one-shot accrual (ZCT-527): once posted, the
auto-reversal executor reverses it in full on/after that date. It must be after
posting_date (422 otherwise), and it can only be set while the entry is a draft —
on the create here or on the PUT replace.
Drafts do not touch the books until …/post. See doc/pipeline.md §"Accounting".
| Name | Required | Description | Default |
|---|---|---|---|
| lines | Yes | ||
| currency | No | ||
| period_id | Yes | ||
| company_id | Yes | ||
| entry_type | No | manual | |
| description | No | ||
| document_id | No | ||
| posting_date | Yes | ||
| auto_reverse_date | No |
customer_statementInspect
One customer's open items + totals (the aging row's drill-down).
as_of mirrors GET …/ar/aging — set, it reconstructs the items open at
that date so the statement agrees with a historical aging report.
| Name | Required | Description | Default |
|---|---|---|---|
| as_of | No | ||
| company_id | Yes | ||
| counterparty_id | Yes |
describe_operationsInspect
Get the full argument schema for operations found via find_operations.
Returns each operation's description and JSON-Schema arguments — everything a listed tool would have shown you.
include_output_schema is the ONLY way to obtain an operation's result
shape: tools/list publishes no output schema for any operation, listed or
not (they are 67 % of the payload, and a schema describing a result is not
what you need to choose a tool). Ask for it when you must know the shape in
advance; the result itself comes back as JSON either way, so usually you do not.
Up to 10 operations per call; anything beyond that is reported in
omitted rather than dropped.
| Name | Required | Description | Default |
|---|---|---|---|
| names | Yes | ||
| include_output_schema | No |
entry_candidatesInspect
Entry Candidates
| Name | Required | Description | Default |
|---|---|---|---|
| entry_id | Yes |
find_operationsInspect
Search EVERY operation this server offers, including the ones not in your tool list.
The tool list you can see holds only the common operations. Several hundred more — payroll, VAT returns, year-end close, fixed assets, expenses, cash book, settings, integrations, onboarding — are available but unlisted, and this is how you find them. Search here before concluding that Zeno cannot do something.
Call with no arguments to browse the catalogue of areas (tags) with their
operation counts; with tag to list one area; with query to search by
keyword. Every word contributes and matching all of them doubles the score,
so extra words widen the search rather than narrowing it — the AND-then-fall-back-
to-OR behaviour this said before was removed in ef47aa9ab. A word matching an
operation's name counts for far more than one found in its description, so
prefer the noun you are looking for over a sentence.
limit is capped at 100 per call; when more matched, next_offset gives
the offset that returns the following page, so an area of any size can be
read in full. Paging is stable — the ranking is deterministic and the registry
is fixed at startup, so no operation is dropped or repeated between pages.
Pass a promising name to describe_operations for its arguments, then
run it with call_operation.
| Name | Required | Description | Default |
|---|---|---|---|
| tag | No | ||
| limit | No | ||
| query | No | ||
| offset | No |
get_ar_invoiceInspect
One authored outgoing invoice with its lines (CompanyMember read).
Carries the header (customer, dates, currency, discounts), every line with its
computed net_amount/vat_amount/gross_amount, the document totals, and
the lifecycle: status (draft → issued → cancelled), document_no
(assigned at issue — null on a draft), commercial_document_id /
snapshot_document_id (the booked document and the frozen PDF), plus the derived
payment_status and open_amount read from the linked open item.
This is the authored arm only. A receivable that arrived as an ingested
document is an item, not an ar_invoice; the two arms are listed together by
GET …/ar/receivables. Use GET …/ar/invoices/{invoice_id}/related for the
journal entry, the settling bank transactions and the credit-note chain.
| Name | Required | Description | Default |
|---|---|---|---|
| company_id | Yes | ||
| invoice_id | Yes |
get_bank_entryInspect
Get Bank Entry
| Name | Required | Description | Default |
|---|---|---|---|
| entry_id | Yes |
get_bank_statusInspect
Composed bank snapshot — "is my banking up to date?" (CompanyMember). "Reconciled" means
entries matched, NOT ledger = statement balance (the close check
BANK_BALANCE_DOES_NOT_TIE_OUT compares that).
One call for what otherwise takes GET …/bank-accounts + one
GET /bank-accounts/{id}/coverage per account + GET …/reconciliation/summary
— and which those three still do not answer: per-account entry-status counts, the
in_suspense counts, each account's latest closing balance, and the company-level
cumulative GL Suspense balance (elsewhere reachable only through a VAT period's
close-checks). Non-zero suspense blocks a clean VAT close.
Read-only. Gaps come from the account's full history: coverage windows are merged and
every internal hole reported (ZCT-509), so a run of missing statements is ONE gap. Not
period-scoped; use …/reconciliation/completeness per month. period_gaps caps at
10/account, period_gap_count stays exact — HOLES, not missing months. Explicit
non-member company_id → 403.
| Name | Required | Description | Default |
|---|---|---|---|
| company_id | Yes |
get_companyInspect
Get one company (jwt, scoped). 403 for a non-member non-admin, 404 if absent.
| Name | Required | Description | Default |
|---|---|---|---|
| company_id | Yes |
get_financial_statementsInspect
Computed balance sheet + P&L for a fiscal year, from the journal (CompanyMember).
Aggregates every posted GL journal line whose posting date falls in the fiscal
year's window, classifies each account against the chart of accounts, and reports
problems (unbalanced trial balance, unclassified accounts, year-not-opened, mixed
currency). Read-only and distinct from the uploaded balance-sheet snapshots
(GET .../balance-sheets): nothing here reads a snapshot. fiscal_year optional
→ the latest FY with journal data. month (YYYY-MM, inside the FY — 422
otherwise) narrows the statement to one month: the balance sheet stays cumulative
(as of month end, YTD equity fold) while the P&L + cash flow cover that month's
flows. Explicit non-member company_id → 403.
The cut — a line is included because its posting_date falls inside the fiscal
year's own window, read off the gl_fiscal_year row; the response's
fiscal_year_cut says so (method="posting_date_window", dates = that window;
the aggregated window is period_start..period_end, narrower when
month-scoped, ZET-166). The /gl/reports/* family cuts by the line's stamped
fiscal_year_id FK instead. Both read the same window, so they agree on where the
year starts and ends; they can still differ on a line dated outside the year it is
stamped to.
| Name | Required | Description | Default |
|---|---|---|---|
| month | No | ||
| company_id | Yes | ||
| fiscal_year | No |
get_gl_account_ledgerInspect
One account's ledger (Kontoauszug) — every booking that touched it, in date order, with Soll/Haben and the running balance after each line (CompanyMember).
No fiscal-year parameter: the chart is FY-owned, so account_id already
fixes the year. Posted and reversed entries are included (a reversal is a
second posted entry, so both legs must show or the balance is wrong); drafts
are not. The running balance is carried across pages by the server —
page_opening_balance enters this page's first row, while
closing_balance/total_debit/total_credit describe the whole
window. 404 when the account is not this company's; 422 when date_from is later
than date_to.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| offset | No | ||
| date_to | No | ||
| date_from | No | ||
| account_id | Yes | ||
| company_id | Yes |
get_gl_journal_entryInspect
One entry + lines, plus the subledger rows it produced/touched (Gap B detail
surfacing): its gl_vat_posting rows, its gl_vat_zero_posting turnover-only
declarations (zero-rated/exempt output — R2-13b: these have no VAT line to pair, so
without them a correctly coded 0 % line is indistinguishable from an untagged one)
and referenced gl_open_item rows. A declared base line also carries the resolved
code on lines[].vat_code. Drafts carry empty lists. documents carries the
accounting documents explicitly attached to the entry (POST …/documents) — the
evidence for a hand-booked entry; a pipeline-created entry carries its document
through source_ref_* instead and lists nothing here. Member read, like the rest of
the detail.
| Name | Required | Description | Default |
|---|---|---|---|
| entry_id | Yes | ||
| company_id | Yes |
get_intakeInspect
Get one intake row by id (scoped to its company: 404/403 split).
| Name | Required | Description | Default |
|---|---|---|---|
| intake_id | Yes |
get_itemInspect
Item metadata (any source).
| Name | Required | Description | Default |
|---|---|---|---|
| item_id | Yes |
get_kpi_reportInspect
Snapshot KPIs + 13-week liquidity forecast, from the GL (CompanyMember).
Cash/liability balances aggregate the FY window of posted/reversed journal lines;
aging + forecast read gl_open_item, pending salary batches and unpaid VAT
periods. Read-only. Explicit non-member company_id → 403.
fiscal_year optional → latest FY with journal data; FY figures as of that
year's end once past. Aging and forecast anchor to today.
| Name | Required | Description | Default |
|---|---|---|---|
| company_id | Yes | ||
| fiscal_year | No |
get_work_listInspect
Everything waiting for this user in this company, most severe first.
| Name | Required | Description | Default |
|---|---|---|---|
| company_id | Yes |
gl_balance_sheetInspect
Assets vs liabilities + equity for one fiscal year, signed by natural side.
Window rules as the trial balance (fiscal_year defaults to the latest posted
year; no all-years mode, ZET-155). With period_id this is a movements
aggregate over that period, not a position.
⚠️ Balances as assets == liabilities_and_equity, NOT liabilities + equity:
the GL has no P&L→equity closing entry, so equity is the equity accounts alone
and net_result is folded into liabilities_and_equity. It is a position
only for a year whose book carries its opening batch; otherwise it is that year's
movements and equity is empty.
Cut by the stamped fiscal_year_id, not posting date (fiscal_year_cut,
ZET-166).
| Name | Required | Description | Default |
|---|---|---|---|
| period_id | No | ||
| company_id | Yes | ||
| fiscal_year | No |
global_searchInspect
Full-text search across emails + items, ranked by ts_rank_cd, scoped (A1).
archive_status constrains item results only (the Documents → Archive page
passes archived so its search box never surfaces pending/triage-parked items).
| Name | Required | Description | Default |
|---|---|---|---|
| q | Yes | ||
| type | No | ||
| limit | No | ||
| offset | No | ||
| source | No | ||
| direction | No | ||
| company_id | No | ||
| archive_status | No |
gl_profit_and_lossInspect
Revenue − expense → net result, for one fiscal year.
Window — same rules as the trial balance: fiscal_year defaults to the latest
posted year, period_id narrows to one period of it, unknown values are 404 and a
contradictory pair 422. A P&L is a flow statement, so no reading makes the sum
of every fiscal year a company's result — hence no all-years mode (ZET-155). Opening
batches post only to balance-sheet accounts, so they never enter this figure.
The cut — by the line's stamped fiscal_year_id FK, not by posting date; the
response's fiscal_year_cut says so (see the trial balance; null only for a
company that has posted nothing, ZET-166). For the full statement — per-account
sections, the year's own account names, the problems list and the cash flow — use
GET /companies/{id}/financial-statements, which cuts by a posting-date window
instead, so the two can disagree for a short/long or shifted fiscal year; this
route returns the three totals.
| Name | Required | Description | Default |
|---|---|---|---|
| period_id | No | ||
| company_id | Yes | ||
| fiscal_year | No |
gl_trial_balanceInspect
Per-account debit/credit columns for one fiscal year, with totals.
fiscal_year (year label) defaults to the latest year posted in; period_id
narrows to one period of it. No all-years mode — account identity is
(company, fiscal year, code), so aggregating double-counts silently (ZET-155).
Unknown year/period → 404; period outside the year → 422.
Cut by the line's stamped fiscal_year_id, not its posting date;
…/financial-statements cuts by posting date inside the same year's stored
window, so the two differ only on a line dated outside the year it is stamped to.
fiscal_year_cut names the cut (ZET-166).
| Name | Required | Description | Default |
|---|---|---|---|
| period_id | No | ||
| company_id | Yes | ||
| fiscal_year | No |
issue_ar_invoiceInspect
Issue a draft — this is the booking step (accountant role).
In one transaction it assigns the next invoice number, posts the journal entry
(debit the AR control account, credit revenue per line, credit output VAT per VAT
code), opens the receivable open item the customer's payment will settle, freezes
the invoice against further edits (status → issued) and renders the PDF+QR
snapshot. The turnover reaches the VAT return from here — a draft never does.
Only a draft can be issued: re-issuing an issued invoice is a 409. It is also
refused when the invoice is not ready (no customer, no lines, a line without a
revenue account…) — call GET …/ar/invoices/{invoice_id}/readiness first, whose
blocking list is exactly what this refuses on — when the issue_date is not
inside an open accounting period, or when the fiscal year has no AR control
account configured.
Returns the issued invoice (now with document_no and
commercial_document_id). A renderer outage does not fail the issue: the booking
stands and the snapshot stays pending, re-rendered on demand by
GET …/ar/invoices/{invoice_id}/pdf. To undo one, reverse its journal entry; there
is no un-issue.
| Name | Required | Description | Default |
|---|---|---|---|
| company_id | Yes | ||
| invoice_id | Yes |
list_ar_invoicesInspect
List authored AR invoices, newest first.
Optional status / doc_type filter by lifecycle + document kind.
Optional period (YYYY-MM) filters by the invoice's issue date (its
month); a malformed value returns 422 {"msg": "period must be YYYY-MM"}.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| offset | No | ||
| period | No | ||
| status | No | ||
| doc_type | No | ||
| company_id | Yes |
list_autopost_decisionsInspect
Recent AI-accountant autonomy decisions (CompanyMember) — the activity feed.
Shows what the autonomy policy posted (posted), would have posted in
assist shadow mode (shadow), or held for human review (held).
ai_no_match shadow rows (one per AI-consulted bank entry with no verdict)
are feed noise and hidden by default; include_no_match=true surfaces them
for audit. outcome / source_ref_id narrow server-side (ZET-75), so the
held-review surfaces can see held rows beyond the newest-limit window.
Newest first, ordered (created_at DESC, id DESC). limit caps at 200 and
offset walks the rest (R2-21): one fiscal year of a mid-size client makes
more decisions than one page holds, so page with offset += limit until a
call returns fewer than limit rows. The response is a bare list.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| offset | No | ||
| outcome | No | ||
| company_id | Yes | ||
| source_ref_id | No | ||
| include_no_match | No |
list_bank_accountsInspect
The company's bank accounts — the bank_account_id every statement import,
bank-entry list and reconciliation call needs (CompanyMember read).
Oldest first, no filter and no paging. Each row carries the iban, currency,
label/bank_name and ledger_account_number — the chart code this account
posts through; without one a bank movement cannot be coded, and it is set with
PUT /bank-accounts/{account_id}.
auto_created marks an account inferred from an imported statement rather than
declared by a human; it starts ownership_status unconfirmed with
needs_ownership_review: true — confirm it before trusting its balances, since an
unconfirmed IBAN may not be the company's at all. statement_count /
entry_count are null here (filled by the single-account
GET /bank-accounts/{account_id}), which is not the same as zero.
| Name | Required | Description | Default |
|---|---|---|---|
| company_id | Yes |
list_bank_entriesInspect
List Bank Entries
| Name | Required | Description | Default |
|---|---|---|---|
| q | No | ||
| to | No | ||
| flag | No | ||
| from | No | ||
| limit | No | ||
| offset | No | ||
| company_id | No | ||
| credit_debit | No | ||
| match_quality | No | ||
| bank_account_id | No | ||
| counterparty_id | No | ||
| bank_statement_id | No | ||
| settlement_status | No | ||
| reconciliation_status | No |
list_companiesInspect
List companies (jwt, scoped). Non-admins see only their member companies.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
list_counterpartiesInspect
List a company's counterparties (filter by kind/q, paginated).
kind repeats to filter on a set (?kind=customer&kind=both) — a
role-shaped picker asks for every kind that plays the role. A single
?kind=supplier keeps its original meaning.
| Name | Required | Description | Default |
|---|---|---|---|
| q | No | ||
| kind | No | ||
| limit | No | ||
| offset | No | ||
| company_id | Yes |
list_gl_accountsInspect
List the postable chart (CompanyMember). fiscal_year filters to one year's chart
(the chart is FY-scoped, so codes repeat across years).
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| offset | No | ||
| company_id | Yes | ||
| fiscal_year | No |
list_gl_journal_entriesInspect
List journal entries.
period_ids narrows to specific fiscal periods, and is how a caller asks for
one fiscal year — the year's period ids (ZET-285). Without it this route
listed, and counted, every fiscal year the company has: the Journal page cut the
year out of the fetched page client-side while total stayed company-wide,
so a year whose entries sat past the first page rendered as "no entries match"
with no way to reach them. The service applies the filter before the count, so
total is the filtered total and the pager is the year's.
untagged_subledger_control=true narrows to the entries that posted to an
AR/AP control account without touching the subledger, under
allow_untagged_control (ZET-150) — the postings a subledger-vs-control
divergence traces back to.
| Name | Required | Description | Default |
|---|---|---|---|
| q | No | ||
| sort | No | posting_date | |
| limit | No | ||
| order | No | desc | |
| offset | No | ||
| status | No | ||
| company_id | Yes | ||
| period_ids | No | ||
| untagged_subledger_control | No |
list_gl_open_itemsInspect
One page of the AR/AP subledger (member read).
direction filters server-side (ZCT-577) — omit it for both. The screen used to
filter the fetched page in the browser, which made the AR chip drop rows and the
total count both directions.
as_of (ZCT-577) reconstructs each item's open amount from posted journal lines
dated on or before that date, instead of reading today's cached amount. Items are
considered regardless of their current status — one settled since that date was
open on it — so status is refused alongside it rather than silently ignored: a
filter that appears to apply and does not is worse than an error. original_amount
and status on each row remain the item's own, which are current facts, not
as-of ones; the screen says so.
| Name | Required | Description | Default |
|---|---|---|---|
| as_of | No | ||
| limit | No | ||
| offset | No | ||
| status | No | ||
| direction | No | ||
| company_id | Yes |
list_gl_periodsInspect
The company's accounting periods — the months a journal entry may post into (CompanyMember read).
A period belongs to one fiscal year and carries period_no (1..12 within the
year), date_from/date_to and a status: open accepts postings,
soft_closed still accepts them, and closed/year_closed refuse them — a
posting date inside a closed period fails until an accountant explicitly reopens
the period (POST …/gl/periods/{period_id}/reopen, reason required, recorded in
the close-run ledger — ZET-161).
fiscal_year_id narrows to one year; omitted, every year's periods come back.
This is not the VAT filing calendar — those are the gl_vat_period rows of
GET …/gl/vat/periods, which have their own quarterly/monthly cadence and their
own filed/paid sealing. Use this list to find the period_id a manual
journal entry needs, and to check why a posting date was refused.
| Name | Required | Description | Default |
|---|---|---|---|
| company_id | Yes | ||
| fiscal_year_id | No |
list_gl_vat_periodsInspect
List Gl Vat Periods
| Name | Required | Description | Default |
|---|---|---|---|
| company_id | Yes |
list_itemsInspect
List/filter items, scoped to the caller's member companies (A1).
Optional period (YYYY-MM) filters by the item's effective date
(invoice date, falling back to the Europe/Zurich-local ingestion date); a
malformed value returns 422 {"msg": "period must be YYYY-MM"}.
exclude_triage_status drops items carrying that triage verdict (NULL-safe)
— the Payables/Receivables pages pass unrelated so relevance-parked items
show only in the Inbox (pipeline §"Relevance gate").
Optional q is free text: a case-insensitive partial (substring) match
over the filename and the counterparty name from both of its sources (the
linked counterparty and the extracted data.company). %/_ are
matched literally, blank is ignored, and total counts the filtered set.
duplicate filters on the row's is_duplicate flag (ZET-413); credit_note on
its is_credit_note flag (ZET-429) — both server-side, so total stays exact.
| Name | Required | Description | Default |
|---|---|---|---|
| q | No | ||
| paid | No | ||
| limit | No | ||
| offset | No | ||
| period | No | ||
| posted | No | ||
| source | No | ||
| direction | No | ||
| duplicate | No | ||
| company_id | No | ||
| credit_note | No | ||
| triage_status | No | ||
| archive_status | No | ||
| counterparty_id | No | ||
| exclude_triage_status | No |
list_payroll_employeesInspect
Every employee on the payroll master data, with today's contract (accountant role — this payload is personal data, so it is not a plain member read).
One row per employee, active and left alike (filter on status /
employment_end yourself — no server-side filter, no paging): master data
(employee_no, display_name, address, ahv_number, birthdate), the
Quellensteuer election (qst_liable + canton/tariff), the salary iban, and
current_contract — the contract valid today, which is where the wage and
workload live (null when none is in force).
readiness lists what is still missing before this employee can be included in a
payroll run — read it before creating one; empty means ready. Note
ahv_pensioner_allowance_waived is genuinely three-state: null means no
election is on record, which blocks the AHV/FAK declaration, and is NOT "not
waived". Use GET …/employees/{employee_id} for children and the full contract
history.
| Name | Required | Description | Default |
|---|---|---|---|
| company_id | Yes |
list_payroll_runsInspect
The company's payroll runs — one per wage period — newest period first (CompanyMember read; the money is aggregate, no per-employee PII).
A run walks draft → calculated → approved → posted → paid →
locked (plus reversed); run_kind separates a regular run from a
correction/reversal, which are always separate runs, never in-place edits, and
are linked by original_run_id / reversed_by_run_id. journal_entry_id is
the wage entry it posted (null before posted), and the lifecycle stamps
(calculated_at, approved_at, posted_at, paid_at, locked_at) say
when each step happened.
Each row also carries employee_count, gross_total/net_total and the
validation error_count/warning_count — a run with errors must not be
approved. staleness is computed at read time: non-null means master data
changed after the run was calculated (a retroactive correction), so the frozen
figures no longer match; it is advisory, and its action says what to do.
Filters: status_filter narrows to one lifecycle state, year to runs whose
period starts in that calendar year. No paging. Use
GET …/payroll/runs/due for wage months that have no run yet.
| Name | Required | Description | Default |
|---|---|---|---|
| year | No | ||
| company_id | Yes | ||
| status_filter | No |
onboarding_guideInspect
How to onboard and create a NEW company: what to ask the user for, which documents to request, and the order to run the setup in.
Discovery does not depend on search: this tool is in the tier, so it is listed for
every caller, and BASE_INSTRUCTIONS names it. find_operations does reach it
("new company setup" ranks it first), but a query made only of common words is
still decided by whichever tool happens to contain them.
Read this before you start a company setup, and before you tell a user that something cannot be done here. It answers the questions the create's own schema cannot: which of fifteen sections actually matter, which documents produce them, and what it costs to skip one.
If your API key's purpose is onboarding, this is the only job it has.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
post_gl_journal_entryInspect
Post a draft entry into the books — accountant. One-way.
A posted entry can no longer be replaced or discarded (both refuse with 409); the only
general correction is …/reverse, which leaves both the original and the reversal
visible. The one exception is PUT …/guessed-account, which re-points a
coding_guessed entry's expense account in place — and it works only on a posted
entry. Check the draft before posting rather than after.
| Name | Required | Description | Default |
|---|---|---|---|
| body | No | ||
| entry_id | Yes | ||
| company_id | Yes |
reconciliation_summaryInspect
Reconciliation summary banner.
Optional from/to scope the per-entry bank-entry counts (total /
matched / missing-invoice / …) to a booking_date window — mirroring the
/bank-entries grid filter — so the banner agrees with a month-filtered table.
The cross-month coverage (bank_accounts gaps / missing_periods) stays
global: gap detection spans statements regardless of the selected month.
| Name | Required | Description | Default |
|---|---|---|---|
| to | No | ||
| from | No | ||
| company_id | Yes |
request_api_keyInspect
Ask the user to hand this session a Zeno API key. Use this first — every current Zeno user already has an account.
email is the address on their Zeno account. Ask them for it; you cannot
guess it, and the request is aimed by it — Zeno will only show it to somebody
signed in as the owner of that address. Pass client_name too: it is the only
thing identifying you on the approval page, and without it the person is asked to
trust "an unnamed client".
Returns a plain verification_uri. Tell the user to open it, sign in, and approve
the request waiting there; we also email them a notice. There is no code, in
either direction: nothing to read out, nothing to type, and nothing for you to put
in a link.
Then call collect_api_key with the claim_token returned here, waiting
poll_interval_seconds between calls, until its status stops being pending. The user chooses on the page what the key may reach — usually a
single company — so expect the key to be scoped, and read a 403 elsewhere as that
scope rather than as a failure.
The answer is the same whether or not that address has a Zeno account. If nothing ever arrives, the likeliest cause is a mistyped address — ask, do not assume.
| Name | Required | Description | Default |
|---|---|---|---|
| Yes | |||
| client_name | No |
reverse_gl_journal_entryInspect
Storno a posted journal entry: post its mirror image (accountant role).
Nothing is deleted; the original stays, linked both ways, and the reversal entry is returned. The only VAT-correct storno door — VAT postings, open items and what the document spawned are all unwound.
body.posting_date dates the reversal and decides its period; omitted, it reuses
the original's date and period.
Refusals, in evaluation order — 409 when: the entry is itself a reversal (post a
new correcting entry instead); the reversal date falls in a filed/paid VAT
period (date it into an open one); an open item still carries a live settlement
(unlink or reverse the payment first); or — only with posting_date — its period
is closed, its fiscal year sealed, or no fiscal year covers it.
| Name | Required | Description | Default |
|---|---|---|---|
| body | No | ||
| entry_id | Yes | ||
| company_id | Yes |
settle_entryInspect
Post this reconciled bank entry into the GL (clean-bank settlement).
Matched → Dr Bank / Cr AR; unmatched → Dr Bank / Cr Suspense; a previously-suspensed
entry now fully matched → Dr Suspense / Cr AR. Idempotent, and a no-op (skipped) when
the GL chart is not configured for native settlement. The matcher still owns
reconciliation_status. See doc/architecture.md §"Posting general ledger".
| Name | Required | Description | Default |
|---|---|---|---|
| entry_id | Yes |
start_signupInspect
Create a Zeno account for somebody who does not have one yet.
Use request_api_key first if they might already have an account — this tool
cannot tell you, and deliberately never will.
We email email a verification link. The user opens it, sees that you asked
(by the name in client_name, shown as an unverified claim), chooses their own
password, and decides separately whether to connect you. You never see the
link, and you must not ask for it: it is what proves they own the mailbox,
and a link you could repeat is a link anyone could.
Then poll collect_api_key with the claim_token returned here. denied is
terminal and means the person chose to create the account without connecting
you, or refused outright — tell them plainly and stop; do not start another.
| Name | Required | Description | Default |
|---|---|---|---|
| Yes | |||
| client_name | No |
upload_documentInspect
Upload an INVOICE or RECEIPT (PDF, image, or .zip of them) for analysis.
file_base64 is the file's raw bytes, base64-encoded. direction is
payable (incoming supplier invoice) or receivable (outgoing).
Creates Item(s) and queues extraction.
This door is invoices-only: it never routes a file onward by type, so a
bank statement, a journal export or a balance sheet sent here is analysed
and then just sits as a document. For anything that is not an invoice — or
when you are not sure what the file is — use upload_intake.
| Name | Required | Description | Default |
|---|---|---|---|
| filename | Yes | ||
| direction | No | payable | |
| company_id | Yes | ||
| file_base64 | Yes |
upload_intakeInspect
Upload ANY file and let the system decide what it is (the unified intake
queue — the same door as "Documents → Upload" in the web app).
file_base64 is the file's raw bytes, base64-encoded.
Prefer this whenever the file is not plainly an invoice, and always for: bank statements (CAMT.053 XML, CSV, PDF or Excel exports) — these become BankStatement + BankEntry rows, matched to the registered bank account by IBAN; journal/accounting exports; and balance sheets, P&L statements and VAT filings. Mixed .zip archives are expanded and routed per file.
Returns 202 with the queue row: routing happens asynchronously, so poll
get_intake for the outcome and its targets. An unroutable file becomes
a failed queue row carrying the reason — it is never silently dropped.
| Name | Required | Description | Default |
|---|---|---|---|
| filename | Yes | ||
| company_id | Yes | ||
| file_base64 | Yes |
whoamiInspect
Which user this request acts as, and which company's books it is bound to (jwt or API key).
Zeno serves every tenant from one host, and an API key is opaque — nothing else
tells a connected agent whose books it is about to write. Call this FIRST when
working over MCP: company_scoped: true means this connection's data access
is confined to company_id — company_name names it (null here means the
bound company no longer exists), no other tenant is reachable, and the admin
endpoints are refused however privileged the owner is. is_admin is that
owner's bit: worth nothing outside this company, still full authority inside it,
so a scoped key with is_admin: true may write these books even when
company_role is null. company_scoped: false means the credentials carry
the owner's full principal and memberships lists the companies it can act
in. Read-only and cheap — at most one lookup. ZET-171, ZET-176.
credential_purpose says which kind of credential this is, and is what the
withholding rule keys on (SP-08): anything that is neither full nor session
is confined, and gets memberships withheld — null, not empty.
Working over MCP, what you book or accept is recorded as MCP — not as this user acting in person — and the app displays it that way.
capabilities says what this caller may do, each verdict computed from the
predicate the gate itself evaluates (SP-12) — so an agent learns what it cannot do
before it collides rather than after. A credential confined to a company gets the
entries whose verdict is a fact about the credential; the one that would read the
owner's estate (create_company) is omitted, because its verdict would disclose a
sibling tenant. A credential confined to no company — an onboarding key — has no
sibling to disclose and does get it, which is the one operation such a key exists to
call. pending_admin_actions is present and empty until PRE-03/PRE-04 fill it.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
zeno_infoInspect
What this server is, which MCP revision it speaks, and how to get a credential.
Needs no credential. Call it first if a tool has just refused you: the
authentication block names the tools that can get this session connected.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
zeno_overviewInspect
What Zeno does, in one page — the platform's capabilities and its limits.
Needs no credential. Read this before telling a user that Zeno cannot do something; it covers VAT, payroll, reconciliation and the AI accountant.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
51 tool updates
- First observed
accept_autopost_decision - First observed
call_operation - First observed
collect_api_key - First observed
create_ar_invoice - First observed
create_gl_journal_entry - First observed
customer_statement - First observed
describe_operations - First observed
entry_candidates - First observed
find_operations - First observed
get_ar_invoice - First observed
get_bank_entry - First observed
get_bank_status - First observed
get_company - First observed
get_financial_statements - First observed
get_gl_account_ledger - First observed
get_gl_journal_entry - First observed
get_intake - First observed
get_item - First observed
get_kpi_report - First observed
get_work_list - First observed
gl_balance_sheet - First observed
gl_profit_and_loss - First observed
gl_trial_balance - First observed
global_search - First observed
issue_ar_invoice - First observed
list_ar_invoices - First observed
list_autopost_decisions - First observed
list_bank_accounts - First observed
list_bank_entries - First observed
list_companies - First observed
list_counterparties - First observed
list_gl_accounts - First observed
list_gl_journal_entries - First observed
list_gl_open_items - First observed
list_gl_periods - First observed
list_gl_vat_periods - First observed
list_items - First observed
list_payroll_employees - First observed
list_payroll_runs - First observed
onboarding_guide - First observed
post_gl_journal_entry - First observed
reconciliation_summary - First observed
request_api_key - First observed
reverse_gl_journal_entry - First observed
settle_entry - First observed
start_signup - First observed
upload_document - First observed
upload_intake - First observed
whoami - First observed
zeno_info - First observed
zeno_overview
Related MCP Connectors
Swiss invoicing and bookkeeping: QR-invoices, clients, expenses, bank import, VAT and reports.
- NumezisOAuthcom.numezis
Swiss SME back office: accounting, VAT, QR-bill invoicing, supplier bills, CRM, HR and payroll.
AI-powered bookkeeping and tax filing for entrepreneurs at the heart of the European economy.
AI agents for bookkeeping, reconciliation, and financial close for SMBs.
Related MCP Servers
- AlicenseCqualityCmaintenanceFree, open-source (MIT), local-first Swiss accounting MCP server: an AI agent posts double-entry journal entries, categorises and chases invoices, and prepares the MWST-Abrechnung (the Swiss VAT return), with a minimalist Studio for human oversight. Posted entries are append-only and immutable, corrections are reversing entries, and every query is tenant-scoped.50047 npmMIT
- AlicenseAqualityBmaintenanceAI agents that automate bookkeeping, bank reconciliation, and month-end financial close for SMBs and CA firms.261MIT
- FlicenseBqualityDmaintenanceMCP server for DACH accounting automation. Connect AI assistants to sevDesk and Lexoffice — create invoices, manage contacts, handle bookings and vouchers for German-speaking businesses.1543 npm-
- AlicenseAqualityDmaintenanceAI bookkeeper for small businesses that connects to QuickBooks Online. Enables users to query financial data like bank balances, P\&L reports, and invoices through natural language in Claude Desktop or Cursor.687 npmMIT
Glama MCP Gateway
Add one secure layer between your agents and this server.