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
| Tier | API 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/accountsRequests 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
| Method | Path | Purpose |
|---|---|---|
| GET | /health | Liveness check (no auth required) |
| GET | /openapi.json | Full OpenAPI 3.1 spec |
| GET | /docs | Interactive API explorer (Swagger UI) |
Authentication
| Method | Path | Purpose |
|---|---|---|
| POST | /auth/login | Username + password → session token (alternative to X-API-Key for human-driven flows) |
| POST | /auth/logout | Invalidate the current session |
| GET | /auth/me | Current authenticated user info |
| POST | /pair | Pair a new device to an existing API key (used by mobile/external clients) |
Chart of accounts
| Method | Path | Purpose |
|---|---|---|
| GET | /accounts | List accounts with filters (type, parent, active) |
| POST | /accounts | Create 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
| Method | Path | Purpose |
|---|---|---|
| GET | /invoices | List invoices with filters |
| POST | /invoices | Create an invoice (auto-posts the journal entry) |
| GET | /invoices/open | Just the unpaid invoices |
| GET | /bills | List bills |
| POST | /bills | Create a bill |
| GET | /bills/open | Just the unpaid bills |
| GET | /estimates | List estimates |
| POST | /estimates | Create an estimate |
| GET | /estimates/{id} | Get one estimate |
| POST | /estimates/{id}/convert | Convert an estimate to an invoice |
| POST | /payments/apply | Apply a payment to one or more invoices/bills |
Contacts and items
| Method | Path | Purpose |
|---|---|---|
| GET | /contacts | List contacts (customers, vendors, employees) |
| POST | /contacts | Create a contact |
| GET | /contacts/{id} | Get one contact |
| PUT | /contacts/{id} | Update a contact |
| GET | /items | List items |
| POST | /items | Create an item |
Reports and balances
| Method | Path | Purpose |
|---|---|---|
| GET | /balances | Account balances at a date |
| GET | /balances/{account_id} | One account's balance + activity |
| GET | /dashboard | Pre-aggregated KPIs for a dashboard view |
| GET | /reports/balances | Trial-balance-shaped output for downstream report tools |
Banking and reconciliation
| Method | Path | Purpose |
|---|---|---|
| GET | /reconciliations | Active and historical reconciliations |
| POST | /reconciliations | Start or complete a reconciliation programmatically |
| GET | /reconciliations/history | Past reconciliations for an account |
Dimensions
| Method | Path | Purpose |
|---|---|---|
| GET | /classes · POST · /classes/{id} GET/PUT | Class management |
| GET | /locations · POST · /locations/{id} | Location management |
| GET | /projects · POST · /projects/{id} | Project management |
Budgets
| Method | Path | Purpose |
|---|---|---|
| GET | /budgets | List budgets |
| POST | /budgets | Create a budget |
| GET | /budgets/{id} | Get one budget with all lines |
| PUT | /budgets/{id} | Replace a budget's lines |
Fixed assets
| Method | Path | Purpose |
|---|---|---|
| GET | /assets | List fixed assets |
| POST | /assets | Register a new asset |
| POST | /assets/{id}/depreciate | Run depreciation for one asset |
| POST | /assets/{id}/dispose | Record disposal |
Settings, sequences, users
| Method | Path | Purpose |
|---|---|---|
| GET | /settings | Read company-level settings |
| PUT | /settings | Update settings |
| GET | /sequences · /sequences/{type} | Inspect or update auto-incrementing sequences (invoice numbers, etc.) |
| GET | /users · POST · /users/{id} | User management (Admin-only) |
| GET | /roles | Role definitions for permission checks |
Search and notifications
| Method | Path | Purpose |
|---|---|---|
| GET | /search?q=... | Full-text search across the file |
| GET | /notifications · /notifications/count | In-app notifications |
| POST | /notifications/{id}/dismiss | Mark a notification dismissed |
Drafts
| Method | Path | Purpose |
|---|---|---|
| GET | /drafts | Saved drafts (in-progress work) |
| POST | /drafts | Save a new draft |
| GET | /drafts/{id} · PUT/DELETE | Manage 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_errorwithconstraint: "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:
- Reverse proxy — run nginx / Caddy on the same machine pointing a hostname at
localhost:21384. Safest for adding TLS without changing Solid. - Bind to all interfaces — Settings → 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. - VPN — keep the bind on
127.0.0.1and 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
| Goal | Approach |
|---|---|
| Stripe → invoices | Stripe webhook hits your service; your service POSTs to Solid's /invoices |
| Payroll → journal entries | Cron job pulls Gusto/OnPay/ADP, posts a manual JE via the API |
| Reporting in Power BI / Looker / Sigma | Daily ETL hits /reports/balances, /balances, /invoices, etc. |
| Custom intake form → contacts | Your form's submit handler POSTs to /contacts |
| Inventory sync from your warehouse | Your 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