SlowBooks Pro v2.31.4 · saved from https://www.slowbookspro.com/docs/features/, where the guide for the latest version always is.

Features overview

Full feature catalog for SlowBooks Pro 2026: everything an evaluator would scroll through, area by area.

The guides go deeper: installing, Server Edition, a cloud server, moving from QuickBooks and MYOB, banking, contractor mode, nonprofit mode, payroll and HR, working with your accountant, encrypting the company file, accessibility, the integrations (bank feeds, Mercury, Stripe, Square, PayPal, the Cloudflare AI gateway), and for a server TLS, operations and security.

Who can do what. Every install has named sign-ins in four roles: administrator (everything), bookkeeper (all the daily books; not users, tokens, settings, backups, companies, encryption, the accountant's copy, Migrate Data, employee records or saved invoice messages), accountant (reads everything but payroll, HR and the employee records; posts journal entries, voids its own, attaches support to them and uses Reclassify Transactions) and read-only (reads everything but the audit log). Payroll, tax forms, benefits, garnishments and onboarding are an administrator's, reads included. A program signs in with an API token (Settings → API Tokens; Authorization: Bearer sbp_…) as bookkeeper, accountant or read-only, never administrator (since v2.29.0; one made as an administrator before then is refused since v2.31.2 and flagged in the list until revoked); a token cannot manage users or tokens or restore a backup, Can make backups lets it make, list and download them, the audit log names it token:<label>, a revoked token answers 401, and on a server with several companies it names its company in X-SlowBooks-Company. Sign-ins end after four hours idle and when a user is deactivated, re-roled or given a new password. A read never writes: a list or report shows a default (the Uncategorized class, the job-cost types, a nonprofit's net-asset accounts) without saving it; the default is saved at the first write that needs it (v2.31.2). Full detail: Server Edition setup.


Invoicing & Payments (Accounts Receivable)

Accounts Payable

Double-Entry Accounting

Payroll & HR

Tax calculations are approximate — verify with a tax professional. Full module reference (models, routes, UI pages, pending items) lives at Payroll and HR.

Banking

Reports & Tax

Keyboard, Back and dialogs

Classes

Nonprofit mode

Construction mode

Dashboard

Analytics

Real-time business intelligence layer that sits on top of the accounting engine. Powered by AnalyticsEngine (app/services/analytics.py) with eleven aggregation methods and 16 operations under /api/analytics/* (seven reads and the nine AI routes). Fully integrated inline into the main SPA as a hash-routed page — no separate shell, no full page reload. Click Analytics & AI in the sidebar to land on #/analytics.

Metrics computed:

Dashboard UI (#/analytics, inline SPA page):

Date-range filtering (all endpoints):

?period=month|quarter|year          (also accepts mtd/qtd/ytd)
?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD    (explicit override)

Unknown period names and missing params fall back to month-to-date. Explicit dates take precedence over named periods.

CSV export:

# every /api/ path needs a sign-in or an API token (Settings → API Tokens, read-only is enough);
# on a server serving several companies add -H "X-SlowBooks-Company: <file>.db"
curl -f -H "Authorization: Bearer $SBP_TOKEN" 'http://localhost:3001/api/analytics/export.csv?period=year' -o analytics.csv

Flat CSV, four # header lines then columns (section, key, subkey, value) covering 10 sections: period, totals, revenue_by_customer, revenue_trend, expenses_by_category, ar_aging, ap_aging, dso, cash_forecast, customer_profit. Drops straight into Excel / Google Sheets / any BI tool.

AI Insights

An optional LLM layer sits on top of the analytics snapshot and produces a compact 3 observations / 3 risks / 3 recommendations executive brief. Nothing is sent until you click the AI Insights button — the feature is zero-cost by default.

What stays local, and what does not (#294). The company file and the REST API never leave the machine. The brief does: when you click AI Insights, the figures in the prompt go to the provider you configured, over public HTTPS only (private and LAN addresses are refused, see the SSRF boundary below), under that provider's data terms. The same is true of an agent CLI operating the local API from the AI setup guide: the agent's connection to SlowBooks is local, but a cloud-backed CLI's model is not, and whatever the agent reads from the books goes to its provider. A "no accounting data leaves this machine" setup needs a model you run locally, pointed at by the agent, and checked to make no outbound calls; running the CLI process locally is not enough by itself. Local credential storage says nothing about where inference happens.

Eight providers supported out of the box, Claude and Grok first:

ProviderWire formatModelsFree tier
Anthropic Claude/v1/messageslisted from GET /v1/models (active models)Paid only
xAI GrokOpenAI-compatlisted from GET /v1/language-models$25 signup credit
Groq (LPU Cloud)OpenAI-compatlisted from GET /openai/v1/models (chat models)Generous free tier, no card
Cloudflare Workers AIOpenAI-compatlisted from the account's model search (Text Generation)10k neurons/day, no card
Cloudflare Worker Gateway (self-hosted)OpenAI-compatlisted from the gateway's /models if it has one, else typedSame 10k neurons/day — your keys stay in your Cloudflare account
OpenAI/v1/chat/completionslisted from GET /v1/models (chat models)Paid only
Google GeminigenerateContentlisted from GET /v1beta/models (generateContent models)Free Flash tier via AI Studio
Custom (OpenAI-compatible)/v1/chat/completionslisted from the endpoint's /models if it has one, else typedAny vendor or gateway that speaks the OpenAI wire format, on the public internet — HTTPS only, private/LAN addresses refused (v2.9, contributed by @jarvis4openclaw)

The model picker lists what your provider offers (v2.22.2, #254). With a key typed or saved, Settings → AI Insights asks the provider's own models endpoint (POST /api/analytics/ai-models with what the page holds; GET for the saved settings) — Save keeps a first key with no model chosen and asks for one from the list — and lists the text models it returns, newest first where the provider says; you choose one, and Refresh models asks again. There is no built-in list and no built-in default, so a vendor renaming or retiring a model is never a code change here. Custom… still takes a typed model ID, which is also what a gateway without a /models endpoint falls back to. A saved model the provider no longer lists is kept and shown as Custom. Cloudflare gets an extra field for your account ID since its endpoint is account-scoped. The dedicated Cloudflare Worker Gateway provider adds a second field for your Worker URL — see the self-hosted gateway section below.

Which providers have run against a live key. Groq was validated end-to-end against a live key at v2.0.0, both the AI Insights button and the predefined-analysis dropdown. OpenAI's request was corrected in v2.17.1 for its current reasoning models (#185, @Sciumo). Claude was run against a live key on the v2.22.1 release build (October 2026): Claude Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5 (Anthropic's current models, the four the menu offers) and the legacy Haiku 4.5 each passed the Test button and AI Insights, and on Sonnet 5.5 a predefined analysis and a tool-calling question answered correctly. v2.22.1 brought its request up to Anthropic's current API: the request v2.22.0 sent is refused by Sonnet 5.5 ("temperature is deprecated for this model"). The others are built to each vendor's published API and unit-tested, without a live-key run by the maintainers. Accepting working PRs that confirm or fix any provider's config — open an issue or PR with provider name, working model ID, and any quirks discovered (e.g., headers, payload shape, error mapping).

Settings encryption. API keys are stored in the settings table under ai_api_key, encrypted with Fernet (AES-128-CBC + HMAC-SHA256) via app/services/crypto.py. Ciphertext rows carry the prefix fernet:v1: so legacy plaintext rows are detected and migrated gracefully. The master key is resolved in priority order:

  1. SETTINGS_ENCRYPTION_KEY environment variable (ops-preferred; the desktop app keeps one in its per-user .env)
  2. .slowbooks-master.key file next to the repo (an install that has one keeps using it)
  3. Derived from PAYROLL_ENCRYPTION_SECRET when that is set to a real secret (v2.18+; how a Docker install keeps its key when the container is recreated). During a payroll-secret rotation the previous secret's key still decrypts, and python -m app.services.encryption rewrap re-encrypts these too.
  4. Fresh generation on first run, written to (2) at 0600 (logged as a warning)

Never commit .slowbooks-master.key — it is in .gitignore. A secret no key on the install decrypts reads as not set (v2.18+): what uses it stops, and Settings names it to be entered again (GET /api/settings/unreadable-secrets).

GET /api/analytics/ai-config returns {provider, model, cloudflare_account_id, worker_url, endpoint_url, has_api_key, api_key_provider, api_key_encrypted, providers} — the raw key is never in the response body. has_api_key is true only when the saved key was entered for the current provider, and api_key_provider names the provider it was entered for. PUT /api/analytics/ai-config accepts a Pydantic AIConfigUpdate model — {provider, model, cloudflare_account_id, worker_url, endpoint_url, api_key} — so malformed payloads are rejected with a 422 before they reach the service layer. A missing api_key keeps the stored key, and an empty string removes it. A key is sent only to the provider it was entered for (v2.22.1): choose another provider and it has no key until one is entered for it, so one company's key never goes to another; switch back and the saved key is used again. A key saved before 2.22.1 belongs to the provider that was chosen when it was saved, read from the audit log; where the log has no such save, to the vendor its prefix names (sk-ant-, xai-, gsk_, AIza; a Custom endpoint on that vendor's own host keeps it); otherwise to none, and api_key_unmatched is true until a key is entered. AI Insights, the analyses and Test all answer 400 "No API key saved for <provider>" when the chosen provider has none.

Endpoints:

All calls go through a hardened httpx.Client with a 60-second timeout (180 for a reasoning model), verify=True (TLS cert validation), follow_redirects=False (no sneaky 302-to-metadata tricks), and a minimal User-Agent. Error messages redact the API key before raising, so logs and 502 responses never leak your secret.

Security hardening (April 2026 external audit pass):

Self-hosted Cloudflare Worker Gateway (per-LAN-owner)

For installations where you don't want any AI credentials stored inside SlowBooks (even encrypted), the cloudflare/ directory ships a minimal Worker that you deploy to your own Cloudflare account. SlowBooks only ever holds a shared secret scoped to your one Worker, so dumping the SQLite file doesn't expose enough to talk to Workers AI as you.

What it buys you:

Setup: wrangler login → openssl rand -hex 32 → wrangler secret put AUTH_TOKEN → wrangler deploy → in Settings → AI Insights choose Provider Cloudflare Worker Gateway (self-hosted), paste the Worker URL and the shared secret, pick a model from the list and Save AI settings. Full step-by-step in Cloudflare AI gateway.

What it does not protect against: a compromised SlowBooks install still has the shared secret, so it can still invoke your Worker — rotate the secret if you suspect compromise; abnormal Worker traffic shows up in the Cloudflare dashboard immediately.

AI Predefined Analyses

Beyond the headline insights brief, the analytics page exposes a curated dropdown of 11 predefined analyses spanning the full ledger surface. Each action pre-fetches its data server-side via the existing app/services/ai_tools.py helpers and sends a focused one-shot prompt to the LLM (no tool calling, no multi-turn). This avoids the brittle "model emits legacy <function=...> syntax" path that some Llama-on-Groq combos still hit, so it works reliably across every provider.

Categories and actions (registered in app/services/ai_actions.py):

CategoryActionTool used
Customers & SalesTop customers by revenueget_sales_by_customer
Unpaid invoices summarysearch_invoices
A/R agingget_aging_report
Financial ReportsExpenses by category (the ledger, by account, v2.22.3)get_expenses_by_category
Vendors & BillsUnpaid bills summarysearch_bills
A/P agingget_aging_report
Banking & CashCash position by accountlist_accounts
Recent payment activitysearch_payments
Financial ReportsP&L analysisget_pl_summary
Balance sheet analysisget_balance_sheet
TaxSales tax positionget_tax_summary

The page-level period selector (MTD / QTD / YTD) flows through to the date-bounded actions automatically; "as of today" actions, and the P&L and Balance sheet analyses, ignore it. Adding a new action is a single ActionSpec registration — point it at one of the existing tool functions and define a one-line framing prompt.

Endpoints:

The 16 underlying read-only tool functions in ai_tools.py are still available (and unit-tested) for any future code path that wants tool calling. The POST /api/analytics/ai-query endpoint that drove the legacy chat panel is retained as a power-user API but is no longer wired into the UI.

PDF export:

curl -f -H "Authorization: Bearer $SBP_TOKEN" 'http://localhost:3001/api/analytics/export.pdf?period=year' -o analytics.pdf

Print-ready PDF via WeasyPrint + Jinja2 (app/templates/analytics_pdf.html → app/services/pdf_service.py::generate_analytics_pdf). Same template handles all periods. Output: letter-size, page numbers in footer, KPI strip + every table the UI shows. ~18 KB typical for a small business; renders in WeasyPrint in well under a second.

Performance: GET /api/analytics/dashboard issues a fixed number of SQL queries (25 measured) regardless of dataset size — every method is single-query (or at most two) with no N+1 relationship loads. Measured on SQLite with 3,000 invoices + 1,500 bills: ~26 ms engine / ~40 ms full HTTP round-trip; with 8,000 invoices + 4,000 bills: ~50 ms. The period parameter adds zero extra queries. PDF export renders end-to-end in ~100 ms on the medium dataset.

Quick smoke test once the app is running:

H='Authorization: Bearer sbp_...'    # a read-only API token from Settings → API Tokens
curl -f -H "$H" http://localhost:3001/api/analytics/dashboard
curl -f -H "$H" 'http://localhost:3001/api/analytics/dashboard?period=year'
curl -f -H "$H" http://localhost:3001/api/analytics/export.csv > snapshot.csv
curl -f -H "$H" http://localhost:3001/api/analytics/export.pdf > snapshot.pdf

(Without the header every /api/ path answers 401, and -o would save that JSON as the file.)

Online Payments

Communication & Export

Inventory, Drill-Down & Duplicate Detection

Duplicate detection warning

Security & Authentication

Canonical list of security measures lives in SECURITY.md; engineering log of the hardening pass is at Security hardening.

System & Administration

UI

QuickBooks 2003 Pro Interoperability

Full IIF format reference, account-type mapping, and round-trip examples live in the QuickBooks Desktop IIF section below.

Utilities


QuickBooks Desktop — IIF Interoperability

SlowBooks exchanges data with QuickBooks Desktop (2003 through the current release) via Intuit Interchange Format (IIF) — the tab-delimited text format QuickBooks Desktop uses for file-based import and export.

Exporting from SlowBooks to QuickBooks Desktop

  1. Navigate to Interoperability in the sidebar
  2. Click Export All Data (or export individual sections)
  3. For the transaction sections, optionally set a date range
  4. Save the .iif file
  5. In QuickBooks Desktop: File > Utilities > Import > IIF Files and select the file

What gets exported:

SectionIIF HeaderFields
Chart of Accounts!ACCNTName, the IIF type for the account's detail type (table below), number, description
Classes!CLASSName, parent
Customers!CUSTName, company, address, phone, email, terms, tax ID
Vendors!VENDName, address, phone, email, terms, tax ID
Items!INVITEMName, type (SERV/PART/OTHC; a DISC item comes in as a service item marked as a discount, #639), description, rate, income account
Invoices!TRNS/!SPL INVOICECustomer:Job, date, line items with item, quantity, price and taxable flag, tax, terms, class
Sales receiptsCASH SALEThe same, with the deposit account
PaymentsPAYMENTCustomer, date, amount, deposit account, invoice allocation
EstimatesESTIMATECustomer, date, line items (non-posting)
BillsBILLVendor, date, due date, lines with their accounts
DepositsDEPOSITBank account, date, the payments taken
Checks and card chargesCHECK / CREDIT CARDBank or card account, payee, date, number, split lines (since 2.24.0)
TransfersTRANSFERFrom and to accounts, date, amount (since 2.24.0)
Bill paymentsBILLPMTBank account, vendor, date, check number (since 2.24.0)
Journal entriesGENERAL JOURNALEvery line with its account, name and class (since 2.24.0)

Importing from QuickBooks Desktop to SlowBooks

  1. In QuickBooks Desktop: File > Utilities > Export > Lists to IIF Files. That menu exports the lists only; transactions come across from a report CSV (Import from Report CSV, above, and Moving from QuickBooks)
  2. In SlowBooks: navigate to Interoperability → Import from IIF
  3. Drag and drop the .iif file (or click to browse), one at a time
  4. Click Validate — checks structure, account types, and balanced transactions. Two boxes follow it: This is an accountant's file posts the file's GENERAL JOURNAL blocks as adjusting entries, and The Customer:Job names in this file are sub-customers, not jobs (v2.31.1) brings a colon name in as a sub-customer that can bill with its parent
  5. If the file has names typed in ALL CAPS, Validate shows a few of them as they would import and offers Change ALL-CAPS names to normal capitalization (BOB JONES → Bob Jones, ACME TOOLING, INC. → ACME Tooling, Inc.). It is off unless you tick it: customer, vendor and account names only, item names kept as typed, and names already in your books are left alone (v2.18.1, #195)
  6. If validation passes, click Import

The importer handles:

tools/clean_iif.py (source installs) writes a cleaned copy of an export with the same quote and capitalization rules, for anyone who wants the file fixed before importing it.

IIF Format Reference

IIF is tab-delimited with \r\n line endings. Header rows start with !. Transaction blocks use TRNS/SPL/ENDTRNS grouping. Sign convention: a positive amount is a debit and a negative one a credit, and a block sums to zero (a sale's TRNS row is the positive receivable; a bill's TRNS row is the negative payable). A minimal block (the real export also carries DUEDATE, TERMS, MEMO and CLASS, and INVITEM, QNTY, PRICE and TAXABLE on the split rows):

!TRNS   TRNSTYPE    DATE    ACCNT   NAME    AMOUNT  DOCNUM
!SPL    TRNSTYPE    DATE    ACCNT   NAME    AMOUNT  DOCNUM
!ENDTRNS
TRNS    INVOICE 01/15/2026  Accounts Receivable John E. Marks   444.96  2001
SPL INVOICE 01/15/2026  Service Income  John E. Marks   -438.00 2001
SPL INVOICE 01/15/2026  Sales Tax Payable   John E. Marks   -6.96   2001
ENDTRNS

Account Type Mapping

Each account's detail type is its IIF type, both ways (the number-range guess an earlier release used is gone, #390):

Detail type (base type)IIF type
Bank (asset)BANK
Accounts Receivable (asset)AR
Other Current Asset (asset)OCASSET
Fixed Asset (asset)FIXASSET
Other Asset (asset)OASSET
Accounts Payable (liability)AP
Credit Card (liability)CCARD
Other Current Liability (liability)OCLIAB
Long Term Liability (liability)LTLIAB
EquityEQUITY
IncomeINC
Other Income (income)EXINC
Cost of Goods Sold (COGS)COGS
ExpenseEXP
Other Expense (expense)EXEXP

Sample Data

The scripts/seed_irs_mock_data.py script populates SlowBooks with test data from IRS Publication 583 (Rev. December 2024) — "Starting a Business and Keeping Records." The sample business is Henry Brown's Auto Body Shop, a sole proprietorship with:

python3 scripts/seed_irs_mock_data.py

API reference

All endpoints under /api/. Swagger docs at /docs, the schema at /openapi.json; 735 operations on 588 paths across 89 routers (v2.31.4). Every endpoint needs a signed-in session or an API token (Authorization: Bearer sbp_…, Settings → API Tokens; the token's role decides what it reaches, and no token is an administrator), except sign-in (/api/auth/*), /health, /openapi.json, the public pay page and its checkout (/pay/{token}, /api/payments/{provider}/create-checkout-session), the provider webhooks (payments, Stripe, Mercury) and the employee portal under /portal/. A refused field answers 422 with a detail list and a plain message per field (v2.31.1). The tables below are a guide, not the whole list: the generated API index has every operation.

Authentication

EndpointMethodsDescription
/api/auth/statusGETAuth state: {setup_needed, authenticated, multi_user}; signed out on a multi-user install, also usernames (names only)
/api/auth/setupPOSTFirst-run password setup (min 8 chars); optional entity_type / llc_tax_election seed the equity accounts (#488)
/api/auth/loginPOSTLogin with password (username too once there is more than one user); on a Server Edition host the status also lists companies, and /api/auth/company chooses one
/api/auth/logoutPOSTClear session (and the person's other sessions)
/api/auth/passwordPOSTChange the signed-in user's password

Core (Original)

EndpointMethodsDescription
/api/dashboard/widgetsGETThe card catalog and the saved layout
/api/dashboard/dataGETThe cards' figures, ?ids= for a subset (snapshot, aging, monthly revenue, bank balances)
/api/settingsGET, PUTCompany settings
/api/settings/test-emailPOSTSend SMTP test email
/api/searchGETUnified search across all entities, classes and accounts included (v2.22.0)
/api/accountsGET, POST, PUT, DELETEChart of Accounts CRUD. ?bank=1 filters to bank/card accounts, ?detail_type= to one detail type. detail_type is optional on POST and PUT (omitted: the default for the type and bank kind, or a sub-account's parent's) and every response carries it with detail_type_label; a detail type that contradicts the type, the bank kind, the 1100 / 2000 rule or the parent is a 400 with the reason. parent_id sets or (as null) clears the parent on PUT; one that would make the account a sub-account of itself, one that does not exist, one of another base type, or a new parent for a control account is a 400 with the reason (v2.27.0). A PUT whose detail-type change cascades to sub-accounts returns retyped_sub_accounts naming each one (id, name, account_number, from/to detail type); it is null on any PUT that did not cascade. PUT refuses the number, type or detail type of a control account (400); POST and PUT refuse creating or renumbering an account onto a reserved control-account number (409); DELETE refuses a control account
/api/accounts/detail-typesGETThe fifteen detail types in picker order, each with its label and base type (v2.27.0)
/api/customersGET, POST, PUT, DELETECustomer management
/api/vendorsGET, POST, PUT, DELETEVendor management
/api/itemsGET, POST, PUT, DELETEItems & services
/api/invoicesGET, POST, PUTInvoice CRUD with line items; another customer's job_id on the header or a line is refused (400), a job that does not exist 404; an edit touching neither customer, job nor lines saves as it is (v2.22.0); an edit takes the bill and ship address fields and ship_to_country, to correct them or to go with a move to another customer, "" or null blanking a part (v2.31.1); a move to another customer is refused (409 money_applied) while a payment or credit memo is applied to the invoice (v2.31.1)
/api/invoices/{id}/pdfGETGenerate invoice PDF
/api/invoices/{id}/voidPOSTVoid with reversing journal entry
/api/invoices/{id}/sendPOSTMark invoice as sent
/api/invoices/{id}/emailPOSTEmail invoice as PDF attachment (/email-preview renders the message first; /api/invoices/apply-late-fees posts the late fees A/R Aging offers, #500)
/api/invoices/{id}/duplicatePOSTDuplicate invoice as new draft, billed as of now (v2.31.1); optional body {allow_zero_total, allow_over_credit_limit}
/api/invoices/{id}/print-previewGETBrowser print preview (HTML)
/api/estimatesGET, POST, PUTEstimate CRUD with line items; a job must be the customer's, header or line (v2.22.0)
/api/estimates/{id}/convertPOSTConvert estimate to invoice (drops unit prices + un-accepted alternates, carries cost type + unit cost, adds the markup lines)
/api/estimates/{id}/acceptPOSTAccept the estimate and lock its job budget (#551); 409 no_job, already accepted, or budget_locked
/api/estimates/{id}/print-previewGETBrowser print preview (HTML)
/api/jobs/{id}/contractGET, PUTThe job's contract terms + summary and schedule of values (#552); PUT validates the type and amounts, defaults a GMP cap to the contract sum, stamps the original sum once
/api/jobs/{id}/contract/from-estimate/{estimate_id}POSTSet the contract from an accepted estimate and build its schedule of values (#552); 409 estimate_not_accepted or estimate_job_mismatch
/api/jobs/{id}/sovPUTReplace the schedule of values (renumbered 1..n, item no defaulted); returns the contract summary (#552)
/api/jobs/{id}/buyoutGETBid vs buyout per trade package (#553): bid cost, bought, variance, committed-remaining, billed-to-date and the commitments behind each (with Sub compliance flags), plus an Unassigned row and totals
/api/change-ordersGET, POSTChange orders (#554): list by job_id (history=true adds superseded PCO revisions), create a PCO / owner / sub change order with lines (cost per type, sell with markup)
/api/change-orders/{id}GET, PUT, DELETERead one; edit a draft/pending one (409 co_locked once approved/void/promoted); delete a draft
/api/change-orders/{id}/submitPOSTDraft → pending
/api/change-orders/{id}/approvePOSTPending → approved; moves the contract, SOV, budget and commitment; 409 promote_instead for a PCO, contingency_exceeded / allowance_exceeded over a limit
/api/change-orders/{id}/rejectPOSTDraft/pending → rejected
/api/change-orders/{id}/voidPOSTReverse every effect of an approved change order; 409 co_billed when an added commitment line has been billed
/api/change-orders/{id}/promotePOSTPromote a PCO to a new owner change order (copies lines, source_pco_id set); the PCO becomes promoted. Body {"pco_ids": [...]} rolls several PCOs (same job, funding, allowance line, markup setting) into the one owner CO
/api/change-orders/{id}/revisePOSTNew draft revision of a PCO (…-R2, supersedes_id); the prior one becomes superseded
/api/change-orders/{id}/sub-orderPOSTFrom an owner change order, create a linked draft sub change order on a chosen commitment_id (its subcontract lines, or line_ids)
/api/change-orders/{id}/pdfGETThe owner CO / PCO document — price (sell) per line and per cost type, and the total (cost and markup stay in the app)
/api/paymentsGET, POSTRecord payments with invoice allocation; Idempotency-Key header makes a POST retry-safe (v2.23.0)
/api/payments/{id}/voidPOSTVoid payment with reversing journal entry (refused while in a deposit)
/api/payments/{id}/applyPOSTApply a payment's unapplied remainder to invoices (allocations)
/api/customers/{id}/creditsGETA customer's unapplied payments and open credit memos
/api/banking/ledger-balanceGETThe ledger balance of a bank/card account as of a date
/api/banking/transactions/{id}PATCHKeep the category picked for a statement line
/api/banking/reconciliations/{id}/report · /pdfGETA completed reconciliation's report
/api/banking/overviewGETBank and card accounts with ledger balances, feed, to-review count
/api/banking/accountsGET, POST, PUTBank feeds (statement identity of a ledger account); opening_balance posts; …/post-legacy-balance
/api/banking/transactionsGET, POSTGET: statement lines (review queue). POST: a register entry — posts a journal entry
/api/banking/transactions/{id}/candidates · match · unmatch · add · exclude · restoreGET, POSTReview-queue actions
/api/banking/accounts/{id}/feed/add-all · auto-matchPOSTBulk review actions
/api/banking/entries/{id}GET, PUTA register entry in the + Entry form's shape; PUT rewrites it in place, a field left out kept (a reconciled entry: amount and date refused, a new category 409 until confirm_reconciled) (part of #388)
/api/banking/entries/{id}/voidPOSTVoid a register entry
/api/banking/reconciliationsGET, POST, DELETEReconciliation sessions over ledger lines (…/{id}/transactions, toggle/{line_id}, mark-all {cleared, through} (v2.22.2), complete); PUT /{id} corrects the statement balance or date while in progress (#389)
/api/transfersGET, POSTTransfers between bank/card accounts (…/{id}/void)

Accounts Payable

EndpointMethodsDescription
/api/purchase-ordersGET, POST, PUTPurchase order CRUD; in construction a commitment carries commitment_type (po / subcontract), trade_package_id, retainage_pct and scope_notes (#553)
/api/purchase-orders/{id}/convert-to-billPOSTConvert PO to bill; optional body lines: [{line_id, account_id, amount}] — amount bills that line's part this period (partial billing, #553), 422 over-remaining / ≤ 0, 409 when nothing is left; retainage held to Retainage Payable (2050)
/api/purchase-orders/{id}/pdf · /print-previewGETPurchase order PDF / print page
/api/billsGET, POST, PUTBill CRUD with line items; a line sends quantity and rate, or amount alone (#391); PUT edits a posted bill (v2.21.0), re-posting its journal; carries retainage_held and lien_waiver_status, and the list/get responses add vendor_compliance (Sub W-9 / COI flags, #553)
/api/bills/{id}/voidPOSTVoid bill
/api/bills/{id}/pdf · /print-previewGETBill PDF / print page
/api/expensesGET, POSTA receipt already paid: DR the expense account (or each line's account, lines, which add up to amount), CR the bank, cash or card account it came from. The older single expense_account_id is accepted as one line; a response carries both lines and the first line's account. No PUT: an expense is voided and entered again
/api/expenses/{id}GETOne expense
/api/expenses/{id}/voidPOSTVoid an expense with a reversing entry; refused once reconciled or in a closed period
/api/bill-paymentsGET, POSTPay vendor bills with allocation (a payment pays its own vendor's bills only); ?bill_id= lists a bill's payments; class_id/job_id are refused (they were accepted and dropped; v2.22.0); Idempotency-Key header makes a POST retry-safe (v2.23.0)
/api/bill-payments/{id}GETOne bill payment with what it paid; POST …/void reverses it (refused once the check is reconciled), PUT …/memo sets the check memo (#602)
/api/credit-memosGET, POSTCredit memo CRUD; a job must be the customer's (v2.22.0); a line takes its own class_id and job_id, else the memo's (v2.31.0, #640)
/api/credit-memos/{id}/applyPOSTApply credit to that customer's invoices
/api/credit-memos/{id}/pdf · /print-previewGETCredit memo PDF / print page
/api/vendor-creditsGET, POSTVendor credit list and create
/api/vendor-credits/{id}GETOne vendor credit with its lines
/api/vendor-credits/{id}/applyPOSTApply a vendor credit to a bill

Productivity

EndpointMethodsDescription
/api/recurringGET, POST, PUT, DELETERecurring invoice templates; a job must be the customer's (v2.22.0); a PUT that ticks Active again rolls the next due date forward by whole periods to the first on or after today, on the start date's day; on a schedule past its end date it is a 409 naming the end date the schedule has, unless it also moves the end date past the next due date or clears it
/api/recurring/generatePOSTGenerate due recurring invoices, one installment per schedule per call; each installment is claimed before it posts, so two calls at once invoice a period once
/api/recurring/{id}/generatePOSTEnter this one schedule's invoice now, due or not: ?occurrence=, the installment the page showed (the next due date when left out), dated its scheduled date or ?dated=; an installment that is no longer the next one is a 409 and nothing is created, and so is one after the next due date, which the 409 says is not the next one (#605)
/api/memorizedGET, POSTEvery schedule in one list (recurring invoices as kind invoice with the mode generate, memorized bills and journal entries with auto or remind), ?due=true and ?kind=; create a memorized bill or journal entry (#605)
/api/memorized/dueGETThe schedules due on or before today
/api/memorized/enter-duePOSTEnter All Due: generate every due recurring invoice and enter every due auto-enter memorized bill and journal entry, every missed occurrence in order however far behind, still going when another window generates an installment first (the app's own runs, when it opens and once a day, enter the memorized ones only)
/api/memorized/entered-recentlyGETWhat the app's last run (startup or daily) entered, for the page's "while you were away" note
/api/memorized/{id}GET, PUT, DELETEOne memorized bill or journal entry with its lines
/api/memorized/{id}/enterPOSTEnter the occurrence the page showed (occurrence, its scheduled date; the next date when left out) now, dated that date or the date sent, and move the schedule on; an occurrence that is no longer the next one is a 409 and nothing is posted, and so is one after the next date, which the 409 says is not the next one
/api/memorized/{id}/pause, /resumePOSTPause; resume rolls the next date forward past the dates missed (a schedule that has ended, or would on resuming, is a 409 with the ended sentence)
/api/batch-paymentsPOSTBatch payment application

Payroll & HR Core

Every route in this group needs an administrator signed in, reads included; a bookkeeper, accountant or read-only sign-in, and every API token, is refused.

EndpointMethodsDescription
/api/employeesGET, POST, PUTEmployee CRUD with W-4, address, hire date, work state (the list is open to bookkeeper and read-only sign-ins as a directory, pay and tax elections blanked)
/api/employees/{id}/ytdGETYear-to-date totals (gross, taxes, deductions, net)
/api/employees/{id}/portal-tokenGET, POSTSelf-service portal token (no company password needed); GET reads what exists, POST makes or rotates it
/api/employees/{id}/bank-accountsGET, POST, DELETEACH direct deposit routing/account numbers (encrypted)
/api/payrollGET, POSTPay run CRUD
/api/payroll/{id}GET, DELETEOne pay run; DELETE discards a draft (its stubs, and the benefit year-to-date, loan balances and time entries it moved, put back; a later draft that counted it is flagged). A processed run is refused (409)
/api/payroll/{id}/processPOSTProcess pay run (creates balanced journal entries)
/api/payroll/{id}/voidPOSTVoid a processed run (#432); 409 with the warnings until ?confirm=true
/api/payroll/{id}/paystub/{stub_id}GETOne stub's PDF
/api/payroll/gross-upPOSTThe gross for a wanted net
/api/payroll/statesGETThe state withholding catalog: method, deductions, other items, SUTA base, source, per state
/api/payroll/{id}/nachaGET, POSTThe NACHA ACH file for a processed pay run: GET from the saved Settings → Direct Deposit details; POST with any of them in the body outranking the saved ones. 409 names what is not set up, or that no one on the run has an account

Payroll & HR

The HR, benefits, deductions, onboarding, time-off, tax-form (/api/tax-forms/*: 941, 940, W-2, W-3, SUI, liability, 1099, 1096) and self-service portal routes are described in Payroll and HR, and every operation is in the API index.

Banking & Deposits

EndpointMethodsDescription
/api/banking/transactions/{id}/addPOSTPost a statement line (category_account_id, payee, memo, class_id, job_id, vendor_id); the entry names vendor_id, else the vendor its payee names (#491)
/api/banking/transactions/vendorsPOSTline_ids of added feed lines: each money-out payee becomes a vendor (or is linked to the vendor of that name) and the entry names it (#491)
/api/bank-rulesGET, POST, PUT, DELETEBank rules (pattern, rule_type, account_id, vendor_id); responses name the account and vendor (#493). A vendor's bank_feed_rule on POST/PUT /api/vendors makes, updates or turns off its rule
/api/vendors/{id}/bank-entriesGETRegister entries naming the vendor — lines added from a bank feed — newest first, with the amount paid and whether voided (#491)
/api/banking/check-registerGETThe register: ledger lines on a bank/card account with running balance, links, cleared state; start_date, end_date, class_id (v2.22.0)
/api/deposits/pendingGETPending deposits in Undeposited Funds
/api/depositsGET, POSTList and create deposits (move funds to bank; line_ids are the payments taken)
/api/deposits/{id}GET, PUTOne deposit with its payments; PUT edits it in place from the register (date, reference, class, job, and which payments it takes via line_ids; a reconciled deposit's amount and date are refused) (part of #388)
/api/deposits/{id}/pdf · /print-previewGETThe deposit slip (v2.22.2): the account, the date, each payment taken and the total; VOID when void
/api/deposits/{id}/voidPOSTVoid a deposit (its payments wait to be deposited again)
/api/cc-chargesGET, POSTCredit card charge entry (card_account_id, …/{id}/void)
/api/checks/printGETGenerate check PDF (3-per-page format) for a bill payment (bill_payment_id), or for a check written in the bank register or as an expense (transaction_id; a deposit, card charge, transfer or void is refused with the reason)
/api/checks/unprinted · /api/checks/print-batch · /api/checks/test-printGET, POST, GETThe checks waiting to print on a bank account with the next number; number and print a batch as one PDF (#603); a sample check for lining up pre-printed stock (#440)

Journal Entries

EndpointMethodsDescription
/api/journalGET, POSTManual journal entry CRUD; is_adjusting marks an adjusting entry (an accountant's defaults to it); an optional entry-level basis (both default, accrual, cash) says which reports the entry counts on (#483)
/api/journal/{id}GET, PUTGet a journal entry with lines; PUT rewrites a hand-entered one in place from the register (a reconciled line's amount, account and the date refused, a non-reconciled account move 409 until confirm_reconciled; the accountant's sign-in does not edit) (part of #388)
/api/journal/{id}/voidPOSTVoid with reversing entry. A document's posting (an invoice's, a bill's, a payment's, a register entry's) is refused with a sentence naming the document to void instead, since its own void reverses it (#661)
/api/fixed-assetsGET, POST, PUTFixed asset register (acquisition posts the purchase)
/api/fixed-assets/{id}/post-purchasePOSTPost the purchase of an asset registered without one
/api/fixed-assets/types · /run-depreciation · /{id}/dispose · /import-csv · /reports/reconciliationGET, POSTAsset types; post depreciation through a date; dispose with gain or loss; import a register; the reconciliation report

Reports & Tax

Shared parameters on the statements: basis=accrual|cash (#483), hide_zero=true (#625), compare=prior_year (#347), exclude_adjusting=true, sort=number|name, and on the P&L columns=month|quarter (#478).

EndpointMethodsDescription
/api/reports/profit-loss (+/pdf, /csv)GETP&L report; class_id gives one class's P&L
/api/reports/profit-loss-by-class (+/pdf, /csv)GETEvery P&L account with a column per class (accounts), and each class's totals (classes); class_ids, show=active, include_empty choose the columns and the response says filtered with columns_total and the unfiltered figures; CSV layout=wide|long (v2.22.0)
/api/reports/profit-loss-by-job (+/pdf, /csv)GETThe same grid with a column per job and "No job"; job_ids (0 = no job), customer_id, show, include_empty (v2.22.0)
/api/reports/balance-sheet (+/pdf, /csv)GETBalance sheet
/api/reports/trial-balance (+/pdf, /csv)GETTrial balance, debit and credit columns with totals
/api/reports/ar-aging (+/csv)GETAccounts receivable aging
/api/reports/ap-aging (+/csv)GETAccounts payable aging
/api/reports/profit-loss-detail, /cash-flow, /sales-by-item, /purchases-by-item, /adjusting-entries, /closing-date-exceptions, /1099-summary, /accountants-package, /reconciliations (each +/pdf and/or /csv)GETThe other reports the Report Center offers; the construction reports (/wip-schedule, /cost-to-complete, /change-order-log, /buyout, /job-cost-by-vendor, /cost-to-complete-forecast, /commitment-status) in construction mode
/api/reports/sales-taxGETSales tax collected
/api/reports/sales-tax/payPOSTRecord sales tax payment to government
/api/reports/general-ledger (+/pdf, /csv)GETEvery posted line by account: balance brought forward, running balance, source, class, period total; account_id and class_id filter it, the balance brought forward under the same class test (v2.22.0)
/api/reports/income-by-customerGETSales totals per customer
/api/tax/schedule-cGETSchedule C data from P&L; 409 not_schedule_c when the company's entity type files another return (#488)
/api/tax/schedule-c/csvGETSchedule C CSV export; 409 not_schedule_c when the company's entity type files another return (#488)
/api/tax/returnGETPage 1 of the return the entity type files (Form 1065, 1120, 1120-S; Schedule C for the entities that file it) from the P&L; 409 no_entity_return when unset or a nonprofit (#488)
/api/tax/return/csvGETThe same as CSV, under the form's title
/api/reports/statement-of-activities (+/pdf, /csv)GETNonprofit: revenue, releases, expenses by restriction
/api/reports/statement-of-financial-position (+/pdf, /csv)GETNonprofit: net assets with / without donor restrictions
/api/reports/fund-balances (+/pdf, /csv)GETNonprofit: per restricted fund
/api/reports/functional-expenses (+/pdf, /csv)GETNonprofit: Form 990 Part IX columns
/api/reports/pledges (+/pdf, /csv)GETNonprofit: promised / received / written off / outstanding
/api/reports/possible-duplicates (+/pdf, /csv)GETPossible duplicate entries on a bank or card account (account_id, start_date, end_date, include_dismissed): groups with each entry's source, reconciled flag and void_path (v2.27.0, #414)
/api/reports/possible-duplicates/dismissPOSTMark a group not a duplicate: {account_id, transaction_ids}
/api/reports/possible-duplicates/dismissals/{id}DELETEPut a dismissed group back on the report
/api/nonprofit/setup-accountsPOSTCreate the net-asset, in-kind and bad-debt accounts if missing
/api/nonprofit/releases (+/suggest, /{id}/void)GET, POSTRelease from restriction
/api/nonprofit/allocation-rules (+/{id}/split, /{id}/preview)GET, POST, PUT, DELETESaved shared-cost rules
/api/nonprofit/allocations (+/{id}/void)GET, POSTPeriod-end functional allocation runs
/api/in-kind-gifts (+/{id}/void)GET, POSTIn-kind gift documents
/api/donors/gifts/{kind}/{id}/acknowledgment/{preview,pdf,email}GET, POSTAcknowledgment letters
/api/donors/{id}/giving-statement/pdf, /api/donors/giving-statements/{pdf,batch-email}, /api/donors/giving-statement/previewGET, POSTYear-end giving statements (preview: one donor's with the default giving-statement message and footer, a sample donor when the file has no gifts)
/api/invoices/{id}/write-offPOSTWrite an open balance off to Bad Debt Expense (any company, v2.22.2)
/api/invoices/write-offs/candidatesGETThe open balances under ?under= as of ?as_of_date=, listed before any is written off (v2.22.2)
/api/invoices/write-offsPOSTWrite the listed invoices off together, one credit memo each; {date, under, invoice_ids, memo} → {written_off, skipped} (v2.22.2)
/api/credit-memos/{id}/voidPOSTVoid a credit memo (unwinds applications)
/api/vendor-credits/{id}/voidPOSTVoid a vendor credit (unwinds applications, returns stock)

Import/Export

EndpointMethodsDescription
/api/iif/export/all · /api/iif/export/{section}GETExport everything as .iif, or one of the fifteen sections
/api/iif/validatePOSTCheck an .iif file without importing; caps_names and caps_name_examples show what the ALL-CAPS box would change
/api/iif/importPOSTImport .iif file; form fields retitle_names=true (ALL-CAPS names in normal capitalization), adjusting=true (an accountant's file) and sub_customers=true (Customer:Job names as sub-customers, v2.31.1)
/api/csv/export/{type}GETExport entities as CSV
/api/csv/import/{type}POSTImport CSV file (`accounts?dry_run=1
/api/csv/import/qb-reportPOSTA QuickBooks Desktop report (CSV or .xlsx): the type detected; a ledger file's problem list and its answers (v2.31.2, #663)
/api/bank-import/preview · /preview-csvPOSTPreview OFX/QFX or CSV transactions (a CSV the import can't read answers its columns for the mapping)
/api/bank-import/import/{id} · /import-csv/{id}POSTImport OFX/QFX or CSV into a bank account

Online Payments

EndpointMethodsDescription
/pay/{token}GETPublic payment page (no auth)
/api/payments/{provider}/create-checkout-sessionPOSTCreate a hosted checkout for an invoice (public; the token is the capability)
/api/payments/{provider}/webhookPOSTProvider webhook handler (/api/stripe/webhook still works)
/api/payments/{provider}/check-status/{invoice_id}POSTAsk the provider whether an invoice was paid
/api/payments/payment-link/{invoice_id}GET, POSTPublic payment URL for an invoice: GET reads one that exists (url null and a note when there is none), POST makes it (an administrator or bookkeeper; a read-only or accountant sign-in is refused)
/api/payments/{provider}/payoutPOSTRecord a payout from the provider's clearing account (#584); the bank review queue's Payout… uses it

System

EndpointMethodsDescription
/api/auditGETAudit log viewer
/api/backupsGET, POSTList this company's backups; make one (an administrator, or a token with Can make backups)
/api/backups/download/{filename}GETDownload a backup file (the same)
/api/backups/restorePOSTRestore a backup: a signed-in administrator only, never a token
/api/companiesGET, POSTMulti-company management
/api/uploads/logoGET, POST, DELETECompany logo: describe, upload, remove (upload and remove: administrator)
/api/uploads/logo/{id}GETThe logo image, from the company's database
/api/attachments/{type}/{id} · /api/attachments/{id} · /api/attachments/download/{id}GET, POST / DELETE / GETList and add a document's attachments; remove one; download one
/api/budgets (+/bulk, /variance)GET, POSTBudgets by account and period, set in bulk, with the actual-vs-budget variance
/api/accounts/rebuild-balancesPOSTRecompute the stored account balances from the ledger (#490; also runs at start)
/api/email-templatesGET, POST, PUT, DELETECustom email template management
/api/invoice-messagesGET, POST, PUT, DELETESaved messages, one the default per kind; ?kind=invoice (default) or ?kind=giving_statement filters the list (#392, #415; writes: administrator)
/healthGETLiveness probe (no auth required)

Inventory

EndpointMethodsDescription
/api/items/{id}/movementsGETPer-item inventory ledger (newest first)
/api/items/{id}/adjustPOSTManual quantity adjustment with offsetting JE
/api/items/low-stockGETItems at or below their reorder point
/api/items/valuationGETSum of qty × avg_cost across tracked items

Drill-Down & Saved Reports

EndpointMethodsDescription
/api/reports/account-transactionsGETEvery journal line hitting an account, with source-doc links for every posting; class_id keeps one class's lines, job_id one job's (0 = no job) (v2.22.0)
/api/customers/check-duplicateGETPre-submit duplicate-name check (fuzzy)
/api/vendors/check-duplicateGETPre-submit duplicate-name check (fuzzy)
/api/saved-reportsGET, POSTList/create named report parameter sets
/api/saved-reports/{id}GET, PUT, DELETESaved report CRUD

Classes (v2.22.0)

EndpointMethodsDescription
/api/classesGET, POSTThe classes (funds in a nonprofit); create one
/api/classes/activityGETOne row per class for start_date–end_date (income, COGS, gross profit, expenses, net); include_archived; totals of the rows and the company's P&L totals
/api/classes/{id}GET, PUT, DELETEOne class; rename or archive it
/api/classes/{id}/summaryGETThe class's P&L for the dates (the same query as /api/reports/profit-loss?class_id) with the company's net income beside it
/api/classes/{id}/transactionsGETEvery posted line attributed to the class across all accounts, with source_link, and the class's debit, credit and net

Analytics

All read endpoints accept ?period=month|quarter|year (or mtd/qtd/ytd; year is the fiscal year when Settings sets one), or explicit ?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD. The AI routes (ai-config, ai-models, ai-config/test) are an administrator's; ai-insights, ai-actions and ai-query are POSTs, closed to read-only and accountant sign-ins and to API tokens.

EndpointMethodsDescription
/api/analytics/dashboardGETComplete analytics snapshot (every metric, includes period echo)
/api/analytics/revenueGETWindowed revenue by customer + 12-month trend
/api/analytics/expensesGETWindowed expense breakdown by account number
/api/analytics/cash-flowGETCash forecast + DSO + A/R and A/P aging (?days=90)
/api/analytics/profitabilityGETLifetime paid revenue per customer
/api/analytics/export.csvGETFlat CSV of the full snapshot (section,key,subkey,value) — honors period params
/api/analytics/export.pdfGETPrint-ready PDF via WeasyPrint — honors period params
/api/analytics/ai-configGET, PUTDisplay config (no raw key) / update provider/model/key/worker_url (key Fernet-encrypted at rest; worker_url validated against SSRF/MITM)
/api/analytics/ai-config/testPOSTSmoke-test the configured provider with a one-word prompt
/api/analytics/ai-modelsGET, POSTThe provider's models, listed by the provider (v2.22.2): GET for the saved settings, POST for what the Settings page holds (a key typed and not yet saved); what the picker shows
/api/analytics/ai-insightsPOSTRun the dashboard through the configured LLM; ?force=true bypasses 10-min cache
/api/analytics/ai-queryPOSTTool-calling Q&A — LLM autonomously calls 16 read-only tools to answer ?question=...
/analyticsGETBackwards-compat 307 redirect to the SPA hash route /#/analytics

Sub-customers (Parent:Sub)

Jobs (Customer:Job / Projects)

Everywhere

Platforms and install