SolaBooks Finance API
The SolaBooks Finance API lets you read and write accounting data programmatically — customers, suppliers, invoices, bills, payments, journal entries, and financial reports. All endpoints return JSON. Create an API key →
Authentication
Every protected endpoint requires three headers:
X-API-Key: solabooks_<prefix>.<secret> X-Organization-Id: 1 X-Client-Id: 4
- X-API-Key — the full key shown once at creation in Settings → API Access
- X-Organization-Id — your organization's integer ID (visible in the API Access page)
- X-Client-Id — your client ID (visible in the API Access page)
The GET /api/v1/health endpoint requires no authentication and can be used to verify connectivity.
Response Format
All responses use a consistent JSON envelope.
Single object
{
"success": true,
"data": { ... }
}
Paginated list
{
"success": true,
"data": [ ... ],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 100,
"last_page": 4
}
}
Error
{
"success": false,
"error": {
"code": "error_code",
"message": "Human readable message."
}
}
Quick Test
Replace your_key, 1, and 4 with your actual API key, organization ID, and client ID.
Health check (no auth required)
curl https://solavel.com/finance/api/v1/health
List invoices
curl https://solavel.com/finance/api/v1/invoices \ -H "X-API-Key: your_key" \ -H "X-Organization-Id: 1" \ -H "X-Client-Id: 4"
Create a customer
curl -X POST https://solavel.com/finance/api/v1/customers \
-H "X-API-Key: your_key" \
-H "X-Organization-Id: 1" \
-H "X-Client-Id: 4" \
-H "Content-Type: application/json" \
-d '{"name":"Acme Corp","email":"billing@acme.com"}'
Create a journal entry
curl -X POST https://solavel.com/finance/api/v1/journal-entries \
-H "X-API-Key: your_key" \
-H "X-Organization-Id: 1" \
-H "X-Client-Id: 4" \
-H "Content-Type: application/json" \
-d '{
"date": "2026-06-07",
"reference": "JE-001",
"description": "Manual accrual",
"lines": [
{"account_id": 101, "debit": 1000, "credit": 0},
{"account_id": 202, "debit": 0, "credit": 1000}
]
}'
All Endpoints
Click any endpoint to expand its details. 33 endpoints total across 8 resource groups.
Health
/api/v1/health
no auth
Confirm the API is online and responding. No authentication required.
Response HTTP 200
{"success":true,"data":{"service":"finance-api","status":"ok","timestamp":"2026-06-07T12:00:00+00:00"}}
Customers
/api/v1/customers
scope: customers.get
List all customers. Supports search and pagination.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| search | string | optional | Filter by name, email, or phone (partial match) |
| page | integer | optional | Page number (default: 1) |
| per_page | integer | optional | Results per page (default: 25, max: 100) |
Response HTTP 200
{"success":true,"data":[{"id":1,"name":"Acme Corp","email":"billing@acme.com","phone":"+1-555-0100","is_active":true}],"meta":{"current_page":1,"per_page":25,"total":3,"last_page":1}}
/api/v1/customers/{id}
scope: customers.get
Retrieve a single customer by ID.
Response HTTP 200
{"success":true,"data":{"id":1,"name":"Acme Corp","email":"billing@acme.com","phone":"+1-555-0100","is_active":true}}
/api/v1/customers
scope: customers.post
Create a new customer.
Request body (JSON)
| Field | Type | Description | |
|---|---|---|---|
| name | string | required | Customer name |
| string | optional | Email address (must be valid email) | |
| phone | string | optional | Phone number (max 50 chars) |
| is_active | boolean | optional | Active status (default: true) |
Response HTTP 201
{"success":true,"data":{"id":42,"name":"Acme Corp","email":"billing@acme.com","phone":null,"is_active":true}}
Suppliers
/api/v1/suppliers
scope: suppliers.get
List all suppliers. Supports search and pagination.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| search | string | optional | Filter by name (partial match) |
| page | integer | optional | Page number |
| per_page | integer | optional | Results per page (max 100) |
Response HTTP 200
{"success":true,"data":[{"id":1,"name":"Global Supply Ltd","email":"ap@globalsupply.com","tax_id":"GB123456789","is_vat_registered":true}],"meta":{"current_page":1,"per_page":25,"total":1,"last_page":1}}
/api/v1/suppliers/{id}
scope: suppliers.get
Retrieve a single supplier by ID.
Response HTTP 200
{"success":true,"data":{"id":1,"name":"Global Supply Ltd","email":"ap@globalsupply.com","phone":null,"tax_id":"GB123456789","is_overseas":false,"is_vat_registered":true,"reverse_charge":false}}
/api/v1/suppliers
scope: suppliers.post
Create a new supplier.
Request body (JSON)
| Field | Type | Description | |
|---|---|---|---|
| name | string | required | Supplier name |
| string | optional | Email address | |
| phone | string | optional | Phone number |
| tax_id | string | optional | VAT / tax registration number |
| is_overseas | boolean | optional | Whether supplier is overseas (default: false) |
| is_vat_registered | boolean | optional | VAT registered (default: true) |
| reverse_charge | boolean | optional | Reverse charge VAT applies (default: false) |
Response HTTP 201
{"success":true,"data":{"id":15,"name":"Global Supply Ltd","email":"ap@globalsupply.com","tax_id":"GB123456789","is_vat_registered":true}}
Invoices
/api/v1/invoices
scope: invoices.get
List all invoices with customer relation. Supports status filter and pagination.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| status | string | optional | Filter: draft | unpaid | paid | void | overdue |
| page | integer | optional | Page number |
| per_page | integer | optional | Results per page (max 100) |
Response HTTP 200
{"success":true,"data":[{"id":101,"number":"INV-00101","date":"2026-06-01","due_date":"2026-07-01","status":"unpaid","total":5000.00,"customer":{"id":1,"name":"Acme Corp"}}],"meta":{"current_page":1,"per_page":25,"total":48,"last_page":2}}
/api/v1/invoices/{id}
scope: invoices.get
Retrieve a single invoice with customer and line items.
Response HTTP 200
{"success":true,"data":{"id":101,"number":"INV-00101","date":"2026-06-01","due_date":"2026-07-01","status":"unpaid","subtotal":4347.83,"tax_total":652.17,"total":5000.00,"customer":{"id":1,"name":"Acme Corp"},"lines":[{"id":1,"description":"Consulting services","qty":10,"unit_price":434.78,"total":4347.83}]}}
/api/v1/invoices
scope: invoices.post
Create a new invoice. Created with status=draft and an auto-generated number.
Request body (JSON)
| Field | Type | Description | |
|---|---|---|---|
| customer_id | integer | required | ID of an existing customer |
| date | date | required | Invoice date (YYYY-MM-DD) |
| due_date | date | optional | Payment due date (YYYY-MM-DD) |
| notes | string | optional | Internal or customer-facing notes |
| subtotal | decimal | optional | Pre-tax total (default: 0) |
| tax_total | decimal | optional | Tax amount (default: 0) |
| discount_total | decimal | optional | Discount amount (default: 0) |
| total | decimal | optional | Grand total (default: 0) |
Response HTTP 201
{"success":true,"data":{"id":102,"number":"INV-00102","status":"draft","date":"2026-06-07","due_date":"2026-07-07","total":5000.00,"customer_id":1}}
/api/v1/invoices/{id}
scope: invoices.update
Update an existing draft invoice. All fields are optional.
Request body (JSON)
| Field | Type | Description | |
|---|---|---|---|
| customer_id | integer | optional | New customer ID |
| date | date | optional | Invoice date (YYYY-MM-DD) |
| due_date | date | optional | Payment due date |
| notes | string | optional | Notes |
| subtotal | decimal | optional | Pre-tax total |
| tax_total | decimal | optional | Tax amount |
| total | decimal | optional | Grand total |
Response HTTP 200
{"success":true,"data":{"id":102,"status":"draft","total":6000.00,"customer":{"id":1,"name":"Acme Corp"},"lines":[]}}
/api/v1/invoices/{id}/post
scope: invoices.post
Post a draft invoice to the general ledger. Requires total > 0 and a date. Changes status to unpaid.
Response HTTP 200
{"success":true,"data":{"id":102,"status":"unpaid","posted_at":"2026-06-07T12:00:00Z","total":5000.00}}
Possible errors:
invoice_already_posted,
invoice_total_required,
invoice_date_required
/api/v1/invoices/{id}/void
scope: invoices.void
Void a posted invoice. Reverses all ledger entries and marks the invoice void.
Response HTTP 200
{"success":true,"data":{"id":102,"status":"void"}}
/api/v1/invoices/{id}/mark-sent
scope: invoices.send
Mark a draft invoice as sent to the customer (changes status from draft to unpaid). Idempotent.
Response HTTP 200
{"success":true,"data":{"id":102,"status":"unpaid"}}
/api/v1/invoices/{id}
scope: invoices.delete
Permanently delete a draft invoice and all its lines. Cannot delete posted invoices or invoices with payment allocations.
Response HTTP 200
{"success":true,"data":{"message":"Invoice deleted."}}
Possible errors:
posted_invoice_not_deletable,
invoice_has_allocations
Bills
/api/v1/bills
scope: bills.get
List all supplier bills. Supports status filter and pagination.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| status | string | optional | Filter: draft | unpaid | paid | void |
| page | integer | optional | Page number |
| per_page | integer | optional | Results per page (max 100) |
Response HTTP 200
{"success":true,"data":[{"id":55,"number":"BILL-00055","date":"2026-06-01","status":"draft","total":2000.00,"supplier":{"id":3,"name":"Global Supply Ltd"}}],"meta":{"current_page":1,"per_page":25,"total":12,"last_page":1}}
/api/v1/bills/{id}
scope: bills.get
Retrieve a single bill with supplier and line items.
Response HTTP 200
{"success":true,"data":{"id":55,"number":"BILL-00055","date":"2026-06-01","due_date":"2026-07-01","status":"draft","reference":"PO-001","total":2000.00,"supplier":{"id":3,"name":"Global Supply Ltd"},"lines":[]}}
/api/v1/bills
scope: bills.post
Create a new supplier bill. Created with status=draft and an auto-generated BILL-XXXXX number.
Request body (JSON)
| Field | Type | Description | |
|---|---|---|---|
| supplier_id | integer | required | ID of an existing supplier |
| date | date | required | Bill date (YYYY-MM-DD) |
| due_date | date | optional | Payment due date |
| reference | string | optional | Supplier reference / PO number (max 120) |
| notes | string | optional | Internal notes |
| subtotal | decimal | optional | Pre-tax total (default: 0) |
| tax_total | decimal | optional | Tax amount (default: 0) |
| discount_total | decimal | optional | Discount (default: 0) |
| total | decimal | optional | Grand total (default: 0) |
Response HTTP 201
{"success":true,"data":{"id":56,"number":"BILL-00056","status":"draft","date":"2026-06-07","total":2000.00,"supplier_id":3}}
/api/v1/bills/{id}
scope: bills.update
Update an existing draft bill. All fields optional.
Request body (JSON)
| Field | Type | Description | |
|---|---|---|---|
| supplier_id | integer | optional | New supplier ID |
| date | date | optional | Bill date |
| due_date | date | optional | Payment due date |
| reference | string | optional | Supplier reference |
| total | decimal | optional | Grand total |
Response HTTP 200
{"success":true,"data":{"id":56,"status":"draft","total":2500.00,"supplier":{"id":3,"name":"Global Supply Ltd"}}}
/api/v1/bills/{id}/post
scope: bills.post
Post a draft bill to the general ledger, registering the payable.
Response HTTP 200
{"success":true,"data":{"id":56,"status":"unpaid","posted_at":"2026-06-07T12:00:00Z"}}
Possible errors:
bill_post_failed
/api/v1/bills/{id}
scope: bills.delete
Permanently delete a draft bill. Bill must be in draft status with no payment allocations.
Response HTTP 200
{"success":true,"data":{"message":"Bill deleted."}}
Possible errors:
posted_bill_not_deletable,
bill_has_allocations
Payments
/api/v1/payments
scope: payments.get
List all payments (receipts and disbursements).
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| type | string | optional | Filter: receipt | disbursement |
| status | string | optional | Filter: draft | posted |
| page | integer | optional | Page number |
| per_page | integer | optional | Results per page (max 100) |
Response HTTP 200
{"success":true,"data":[{"id":20,"type":"receipt","payment_date":"2026-06-07","amount_total":5000.00,"status":"draft","customer_id":1}],"meta":{"current_page":1,"per_page":25,"total":8,"last_page":1}}
/api/v1/payments
scope: payments.post
Record a payment receipt (customer paid) or disbursement (you paid supplier). Optionally allocate against an invoice or bill.
Request body (JSON)
| Field | Type | Description | |
|---|---|---|---|
| type | string | required | "receipt" (customer payment) or "disbursement" (supplier payment) |
| payment_date | date | required | Date of payment (YYYY-MM-DD) |
| amount_total | decimal | required | Total amount paid (must be > 0) |
| method | string | optional | Payment method e.g. bank_transfer, cash, card |
| reference | string | optional | Bank reference or cheque number (max 120) |
| description | string | optional | Description or memo |
| customer_id | integer | cond. | Required for receipt payments (inferred from invoice if document_type=ar_invoice) |
| supplier_id | integer | cond. | Required for disbursement payments (inferred from bill if document_type=ap_bill) |
| document_type | string | optional | "ar_invoice" or "ap_bill" — triggers automatic allocation |
| document_id | integer | optional | ID of the invoice or bill to allocate against (required when document_type is set) |
| allocated_amount | decimal | optional | Amount to allocate (defaults to amount_total) |
Response HTTP 201
{"success":true,"data":{"id":21,"type":"receipt","payment_date":"2026-06-07","amount_total":5000.00,"status":"draft","allocations":[{"document_type":"ar_invoice","document_id":101,"allocated_amount":5000.00}]}}
Possible errors:
document_required,
document_type_mismatch,
invoice_not_found,
bill_not_found,
client_required,
supplier_required
Journal Entries
/api/v1/journal-entries
scope: journal_entries.get
List journal entries with lines and accounts. Supports date range filtering.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| status | string | optional | Filter: draft | posted |
| from | date | optional | Start date (YYYY-MM-DD) |
| to | date | optional | End date (YYYY-MM-DD) |
| page | integer | optional | Page number |
| per_page | integer | optional | Results per page (max 100) |
Response HTTP 200
{"success":true,"data":[{"id":5,"entry_date":"2026-06-07","reference":"JE-001","status":"posted","lines_count":2}],"meta":{"current_page":1,"per_page":25,"total":5,"last_page":1}}
/api/v1/journal-entries
scope: journal_entries.post
Create and immediately post a balanced journal entry. Total debits must equal total credits (tolerance ±0.01).
Request body (JSON)
| Field | Type | Description | |
|---|---|---|---|
| date | date | required | Entry date (YYYY-MM-DD) |
| reference | string | optional | Reference code e.g. JE-001 (max 120) |
| description | string | optional | Memo / description (max 2000) |
| lines | array | required | Minimum 2 lines. Each line: account_id (required), debit (decimal), credit (decimal), description (string) |
Response HTTP 201
{"success":true,"data":{"id":6,"entry_date":"2026-06-07","reference":"JE-001","status":"posted","posted_at":"2026-06-07T12:00:00Z","lines":[{"account_id":101,"account":{"code":"1100","name":"Cash"},"debit":1000,"credit":0},{"account_id":202,"account":{"code":"3000","name":"Equity"},"debit":0,"credit":1000}]}}
Possible errors:
journal_not_balanced
/api/v1/journal-entries/{id}
scope: journal_entries.delete
Delete an unposted journal entry. Posted entries cannot be deleted through the API.
Response HTTP 200
{"success":true,"data":{"message":"Journal entry deleted."}}
Possible errors:
posted_journal_not_deletable
Reports
All report endpoints accept ?from=YYYY-MM-DD&to=YYYY-MM-DD. Balance sheet, trial balance, and aging reports also accept ?as_of=YYYY-MM-DD (equivalent to to).
/api/v1/reports/income-statement
scope: reports.income_statement
Profit and loss report. Returns revenue, expenses, and net income for the specified period.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| from | date | optional | Period start (YYYY-MM-DD) |
| to | date | optional | Period end (YYYY-MM-DD) |
Response HTTP 200
{"success":true,"data":{"revenue":150000,"expenses":85000,"net_income":65000}}
/api/v1/reports/balance-sheet
scope: reports.balance_sheet
Balance sheet snapshot. Returns assets, liabilities, and equity as of the specified date.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| to | date | optional | As-of date (YYYY-MM-DD). Also accepted as ?as_of= |
Response HTTP 200
{"success":true,"data":{"assets":{"total":250000},"liabilities":{"total":80000},"equity":{"total":170000}}}
/api/v1/reports/cash-flow
scope: reports.cash_flow
Cash flow statement for the specified period (direct method).
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| from | date | optional | Period start |
| to | date | optional | Period end |
Response HTTP 200
{"success":true,"data":{"operating":45000,"investing":-12000,"financing":-8000,"net_change":25000}}
/api/v1/reports/trial-balance
scope: reports.trial_balance
Trial balance showing all accounts with debit and credit totals as of a date.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| to | date | optional | As-of date (YYYY-MM-DD) |
Response HTTP 200
{"success":true,"data":[{"account_code":"1100","account_name":"Cash","debit":50000,"credit":0},{"account_code":"2000","account_name":"Accounts Payable","debit":0,"credit":15000}]}
/api/v1/reports/ar-aging
scope: reports.ar_aging
Accounts receivable aging. Outstanding customer invoices bucketed by how overdue they are.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| to | date | optional | Aging as-of date (YYYY-MM-DD) |
Response HTTP 200
{"success":true,"data":{"current":12000,"1_30":3500,"31_60":800,"61_90":0,"over_90":500,"total":16800}}
/api/v1/reports/ap-aging
scope: reports.ap_aging
Accounts payable aging. Outstanding supplier bills bucketed by how overdue they are.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| to | date | optional | Aging as-of date (YYYY-MM-DD) |
Response HTTP 200
{"success":true,"data":{"current":8000,"1_30":1200,"31_60":0,"61_90":0,"over_90":0,"total":9200}}
/api/v1/reports/tax
scope: reports.tax
VAT / tax summary. Returns tax collected on sales and tax paid on purchases for the period.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| from | date | optional | Period start |
| to | date | optional | Period end |
Response HTTP 200
{"success":true,"data":{"tax_collected":15000,"tax_paid":4200,"net_vat_payable":10800}}
Error Reference
| Error Code | HTTP | Meaning |
|---|---|---|
| invoice_already_posted | 422 | Invoice is already posted to the ledger |
| invoice_total_required | 422 | Cannot post an invoice with zero or negative total |
| invoice_date_required | 422 | Document date is required before posting |
| posted_invoice_not_deletable | 422 | Void or unpost the invoice before deleting |
| invoice_has_allocations | 422 | Remove payment allocations before deleting |
| posted_bill_not_deletable | 422 | Cannot delete a posted or non-draft bill |
| bill_has_allocations | 422 | Remove payment allocations before deleting |
| bill_post_failed | 422 | Bill failed internal post validation |
| journal_not_balanced | 422 | Total debits do not equal total credits |
| posted_journal_not_deletable | 422 | Only draft (unposted) entries can be deleted |
| document_required | 422 | document_id is required when document_type is set |
| document_type_mismatch | 422 | ar_invoice requires receipt; ap_bill requires disbursement |
| invoice_not_found | 404 | Invoice with that ID does not exist |
| bill_not_found | 404 | Bill with that ID does not exist |
| client_required | 422 | customer_id is required for receipt payments |
| supplier_required | 422 | supplier_id is required for disbursement payments |
Pagination
All list endpoints return paginated results. Control pagination with query parameters:
| Parameter | Default | Max | Description |
|---|---|---|---|
| page | 1 | — | Page number to retrieve |
| per_page | 25 | 100 | Number of results per page |
The meta object in every list response contains current_page, per_page, total, and last_page.