REST API

Solid Accounting Professional and Accountant editions ship with an embedded REST API server running on localhost:21384. It gives you programmatic read/write access to every meaningful piece of your accounting data — accounts, invoices, bills, contacts, reports, balances, attachments — with the same double-entry balance validation, closed-period checks, and audit-trail logging that the UI enforces.

This is what lets you wire Solid into the rest of your stack: a Stripe webhook that auto-creates invoices, a payroll integration that posts batch journal entries, a Power BI / Looker pipeline pulling balances daily, a custom intake form that creates customers.

Tier availability

TierAPI access
Standard
Professional✓ Local API
Accountant✓ Local API

The API is local-only by default (binds to 127.0.0.1:21384). Remote access requires a deliberate configuration change — see Network access below.

Enabling the API

In Solid: Settings → API & Integrations → Enable Integration API. The first time you enable it, Solid generates an API key (a 64-character random string) and shows it once. Copy it and store it like any other credential — Solid hashes the key before storing, so it can't be retrieved again. Lose it and you generate a new one.

After enabling:

  • The API server starts on 127.0.0.1:21384
  • The OpenAPI spec is served at /openapi.json
  • A live API explorer is at /docs

The server runs as long as Solid Accounting is open. Closing the app stops the server (the API isn't a separate background service).

Base URL

All API endpoints are versioned under /api/v1:

http://localhost:21384/api/v1

The current version is v1. We follow semver-style API versioning — additive changes don't break v1; breaking changes (renamed fields, removed endpoints) ship as v2 with v1 still served for a deprecation window.

Authentication

Every request sends the API key in an X-API-Key header:

curl -H "X-API-Key: $SOLID_API_KEY" \
  http://localhost:21384/api/v1/accounts

Requests without a valid key get 401 Unauthorized. Requests with an expired key get 401 with an explanatory body. The key has no expiration by default; you can revoke it any time from Settings → API & Integrations.

For browser-based clients (a local web app talking to the API), CORS is restricted to the same origin by default. Allowed origins can be added under Settings → API & Integrations → CORS Allowlist.

Response format

Every endpoint returns JSON with this envelope:

{
  "success": true,
  "data": { ... },
  "meta": {
    "request_id": "01J9X...",
    "timestamp": "2026-04-30T18:42:11Z"
  }
}

On error:

{
  "success": false,
  "error": {
    "code": "validation_error",
    "message": "Journal entry does not balance: debits 100.00 != credits 99.00",
    "details": {
      "field": "lines",
      "constraint": "double_entry_balance"
    }
  },
  "meta": { "request_id": "...", "timestamp": "..." }
}

The success boolean is your fast-path check. The error.code field is a stable, programmatic identifier you can switch on; message is human-readable and may change between versions.

Endpoint groups

The API surfaces ~50 endpoints organized into these groups:

Health and discovery

MethodPathPurpose
GET/healthLiveness check (no auth required)
GET/openapi.jsonFull OpenAPI 3.1 spec
GET/docsInteractive API explorer (Swagger UI)

Authentication

MethodPathPurpose
POST/auth/loginUsername + password → session token (alternative to X-API-Key for human-driven flows)
POST/auth/logoutInvalidate the current session
GET/auth/meCurrent authenticated user info
POST/pairPair a new device to an existing API key (used by mobile/external clients)

Chart of accounts

MethodPathPurpose
GET/accountsList accounts with filters (type, parent, active)
POST/accountsCreate an account
GET/accounts/{id}Get one account
PUT/accounts/{id}Update an account
DELETE/accounts/{id}Deactivate (soft-delete) — accounts with history can't be hard-deleted

Transactions

MethodPathPurpose
GET/invoicesList invoices with filters
POST/invoicesCreate an invoice (auto-posts the journal entry)
GET/invoices/openJust the unpaid invoices
GET/billsList bills
POST/billsCreate a bill
GET/bills/openJust the unpaid bills
GET/estimatesList estimates
POST/estimatesCreate an estimate
GET/estimates/{id}Get one estimate
POST/estimates/{id}/convertConvert an estimate to an invoice
POST/payments/applyApply a payment to one or more invoices/bills

Contacts and items

MethodPathPurpose
GET/contactsList contacts (customers, vendors, employees)
POST/contactsCreate a contact
GET/contacts/{id}Get one contact
PUT/contacts/{id}Update a contact
GET/itemsList items
POST/itemsCreate an item

Reports and balances

MethodPathPurpose
GET/balancesAccount balances at a date
GET/balances/{account_id}One account's balance + activity
GET/dashboardPre-aggregated KPIs for a dashboard view
GET/reports/balancesTrial-balance-shaped output for downstream report tools

Banking and reconciliation

MethodPathPurpose
GET/reconciliationsActive and historical reconciliations
POST/reconciliationsStart or complete a reconciliation programmatically
GET/reconciliations/historyPast reconciliations for an account

Dimensions

MethodPathPurpose
GET/classes · POST · /classes/{id} GET/PUTClass management
GET/locations · POST · /locations/{id}Location management
GET/projects · POST · /projects/{id}Project management

Budgets

MethodPathPurpose
GET/budgetsList budgets
POST/budgetsCreate a budget
GET/budgets/{id}Get one budget with all lines
PUT/budgets/{id}Replace a budget's lines

Fixed assets

MethodPathPurpose
GET/assetsList fixed assets
POST/assetsRegister a new asset
POST/assets/{id}/depreciateRun depreciation for one asset
POST/assets/{id}/disposeRecord disposal

Settings, sequences, users

MethodPathPurpose
GET/settingsRead company-level settings
PUT/settingsUpdate settings
GET/sequences · /sequences/{type}Inspect or update auto-incrementing sequences (invoice numbers, etc.)
GET/users · POST · /users/{id}User management (Admin-only)
GET/rolesRole definitions for permission checks

Search and notifications

MethodPathPurpose
GET/search?q=...Full-text search across the file
GET/notifications · /notifications/countIn-app notifications
POST/notifications/{id}/dismissMark a notification dismissed

Drafts

MethodPathPurpose
GET/draftsSaved drafts (in-progress work)
POST/draftsSave a new draft
GET/drafts/{id} · PUT/DELETEManage individual drafts

The full schema with request/response shapes for every endpoint is in the OpenAPI spec (/openapi.json); the live explorer (/docs) lets you call any endpoint with your API key and see real responses.

Conventions

A few patterns to know:

IDs are UUIDs

Every resource has a TEXT PRIMARY KEY UUID. References between resources (invoice → customer, journal entry line → account) use these UUIDs. The API surfaces them as strings; don't try to parse semantics from them.

Money is integer cents

Every monetary field is an integer count of cents (so i64 in the database, number in JSON, but always whole-number cents). 12345 means $123.45. Never use floating-point math for money — Solid never does.

Dates are ISO 8601

YYYY-MM-DD for date-only, YYYY-MM-DDTHH:MM:SS.fffZ for timestamps. Always UTC for timestamps.

Currency

Every monetary field is in the resource's currency. The currency code is on the resource itself (e.g. invoice.currency_code = "USD"). Multi-currency exchange rates are stored as i64 × 1,000,000 (so 1.234567 = 1234567).

Pagination

List endpoints support ?limit= and ?offset= query parameters. Default limit is 50; max is 500. Responses include meta.total_count so you can paginate.

Filtering

List endpoints accept resource-specific filter parameters: ?status=posted, ?from=2026-01-01&to=2026-01-31, ?customer_id=..., etc. The OpenAPI spec lists every filter per endpoint.

Double-entry validation

This is the API's signature behavior: every write operation enforces the same accounting rules the UI does.

  • Creating an unbalanced journal entry → 400 validation_error with constraint: "double_entry_balance"
  • Posting to a closed period without override → 403 closed_period
  • Modifying a posted entry → not allowed; you'd post a reversing + corrected entry instead
  • Every write logs to the audit trail with the user/key that made the change

This means the API is safe to expose to integrations that aren't tightly trusted — they can't break your books by sending malformed data; the worst they can do is fail and surface a clean error.

Network access

By default, the API listens on 127.0.0.1 only — local clients on the same machine can reach it; nothing else. Three ways to expose it more widely:

  1. Reverse proxy — run nginx / Caddy on the same machine pointing a hostname at localhost:21384. Safest for adding TLS without changing Solid.
  2. Bind to all interfacesSettings → API & Integrations → Bind Address. Switches to 0.0.0.0:21384. Useful for trusted office LANs; never expose this to the public internet without a proxy.
  3. VPN — keep the bind on 127.0.0.1 and require clients to connect through a VPN (Tailscale, WireGuard, etc.). Most secure for cross-office integrations.

We recommend option 1 (reverse proxy) for any non-localhost access, with TLS terminated at the proxy.

Rate limits

Defaults:

  • 600 requests per minute per API key for authenticated endpoints
  • No limit on /health

Hitting the limit returns 429 Too Many Requests with a Retry-After header. Limits can be raised per key from Settings → API & Integrations; for large bulk imports, the cleaner pattern is to use the .qbextract migration tool instead.

Webhooks (planned)

Outbound webhooks — Solid POSTing to your URL when something happens — are on the roadmap but not in v1. The current pattern is to poll /notifications/check periodically for change events.

Common integrations

GoalApproach
Stripe → invoicesStripe webhook hits your service; your service POSTs to Solid's /invoices
Payroll → journal entriesCron job pulls Gusto/OnPay/ADP, posts a manual JE via the API
Reporting in Power BI / Looker / SigmaDaily ETL hits /reports/balances, /balances, /invoices, etc.
Custom intake form → contactsYour form's submit handler POSTs to /contacts
Inventory sync from your warehouseYour warehouse system POSTs inventory adjustments via /items + /balances

The key insight: Solid's API is a complete read/write surface, not a curated subset. If you can do it in the UI, you can do it via the API.

Cross-references

  • General Ledger → audit trail — every API write goes through the same audit hook
  • Multi-User module — API access works against a Host-Mode or Dedicated Server instance the same way
  • The OpenAPI spec at http://localhost:21384/openapi.json (when API is enabled) is the source of truth for request/response shapes
Updated May 1, 2026
Edit this page on GitHub →
Was this helpful?

We use this to prioritize which docs to improve. No tracking, no email follow-up.