Home Integration API reference
REST API for integrations and external apps
Expenses
Lists expense and expense_refund transactions that are visible to the token user.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/expenses |
| Permission | all_expense.access or view_own_expense. |
| Visibility | Rows are limited to permitted locations. Non-admin users without all_expense.access only see rows where they are the creator or the expense_for user. |
| Type coverage | Returns both type = expense and type = expense_refund rows unless filtered. |
| CSV behavior | format=csv streams all matching rows with a UTF-8 BOM and ignores page and per_page. Nested objects are JSON-encoded in CSV cells. |
| Success response | 200 with paginated ExpenseRow rows. |
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page, page | integer | No | Pagination controls. per_page accepts 1 to 100 and defaults to 20. |
location_id, contact_id, expense_for, created_by, expense_category_id, expense_sub_category_id | integer | No | Optional location, contact, user, and category filters. |
start_date, end_date | string | No | Optional Y-m-d bounds on transaction_date. The filter is only applied when both values are present. |
payment_status | string | No | paid, due, or partial. |
type | string | No | expense or expense_refund. |
q | string | No | Minimum 2 characters when sent. Matches ref, document, notes, numeric id, linked contact fields, and category/sub-category names. |
format | string | No | json (default) or csv. |
ExpenseRow object| Field | Type | Description |
|---|---|---|
id | integer | Expense transaction id. |
type | string | null | expense or expense_refund. |
ref_no, document | string | null | Stored reference and document fields. |
transaction_date | string | null | ISO-8601 transaction timestamp. |
payment_status | string | null | Current payment state. |
final_total, total_before_tax, tax_amount, total_paid | number | null | Expense totals from the transaction header and payment subquery. |
additional_notes | string | null | Saved notes. |
is_recurring | boolean | Whether this expense is configured as recurring. |
created_by | integer | null | Creator user id. |
expense_category, expense_sub_category | object | null | Category summaries with id and name. |
expense_for | object | null | User summary with id and combined display name. |
contact | object | null | Contact summary with id, name, mobile, contact_id, and supplier_business_name. |
location | object | null | Business-location summary with id and name. |
| Field | Type | Description |
|---|---|---|
data | array<ExpenseRow> | The current result page. |
meta.current_page, meta.last_page, meta.per_page, meta.total | integer | Laravel paginator metadata. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The expense list was returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks expense-list access. | { "message": string } |
422 | The query string failed validation or q was shorter than 2 characters. | Laravel validation JSON or { "message": string }. |
Returns one visible expense or expense refund with recurring metadata and payment history.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/expenses/{id} |
| Permission | Same permission and visibility rules as List expenses. |
| CSV behavior | format=csv streams one UTF-8 BOM row whose data_json column matches the JSON data payload. |
| Success response | 200 with one ExpenseDetail object. |
ExpensePaymentSummary object| Field | Type | Description |
|---|---|---|
id | integer | Payment-line id. |
amount | number | null | Stored payment amount. |
method | string | null | Payment method key. |
paid_on | string | null | ISO-8601 payment timestamp. |
payment_ref_no | string | null | Generated payment reference number. |
note | string | null | Optional payment note. |
is_return | boolean | Whether the payment is recorded as a return/negative payment line. |
ExpenseDetail object| Field | Type | Description |
|---|---|---|
id, type, ref_no | integer | string | Expense identifiers and type. |
document | string | null | Stored document filename or path. |
transaction_date | string | null | ISO-8601 transaction timestamp. |
payment_status | string | null | Current payment state. |
final_total, total_before_tax, tax_amount, discount_amount, total_paid | number | null | Expense totals from the header and payment lines. |
additional_notes, staff_note | string | null | Saved notes. |
is_recurring | boolean | Whether this expense is configured as recurring. |
recur_interval, recur_repetitions, recur_parent_id | integer | null | Recurring schedule counters and parent recurring invoice id. |
recur_interval_type | string | null | days, months, or years. |
subscription_repeat_on | integer | null | Repeat day-of-month value used by monthly recurring expenses. |
created_by | integer | null | Creator user id. |
expense_category, expense_sub_category | object | null | Category summaries with id and name. |
expense_for | object | null | User summary with id, combined display name, and email. |
contact | object | null | Contact summary with id, name, mobile, email, contact_id, and supplier_business_name. |
location | object | null | Business-location summary with id and name. |
order_tax | object | null | Tax summary with id, name, amount, and is_tax_group. |
payments | array<ExpensePaymentSummary> | Payment lines linked to the expense. |
| Field | Type | Description |
|---|---|---|
data | ExpenseDetail | The requested expense payload. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The expense was returned successfully. | { "data": { ... } } or CSV download. |
403 | The token user lacks expense-list access. | { "message": string } |
404 | The expense id does not exist in the visible query. | { "message": "Not found" } |
422 | The query string failed validation. | Laravel validation JSON. |
Creates a new expense or expense refund and can optionally post initial payment lines in the same request.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/expenses |
| Permission | expense.add. |
| Subscription | The business must have an active subscription. |
| Demo mode | Returns 403 in demo environments. |
| Location rule | location_id must belong to the business and must be included in the token user's permitted locations. |
| Type behavior | is_refund = true creates type = expense_refund; otherwise the controller creates type = expense. The transaction is stored with status = final. |
| Recurring behavior | When is_recurring is truthy, the create pipeline stores recurring fields and defaults recur_interval to 1 when omitted. |
| Upload behavior | The underlying util reads an optional multipart document upload when present, even though the integration controller does not add a dedicated validation rule for that file. |
| Success response | 201 with one ExpenseDetail object. |
| Field | Type | Required | Description |
|---|---|---|---|
location_id | integer | Yes | Business location id. |
final_total | number | Yes | Total expense amount including tax. |
ref_no | string | null | No | Optional expense reference number. Auto-generated when omitted. |
transaction_date | string | null | No | Optional date string passed through the same date-formatting pipeline as the web expense form. When omitted, the current timestamp is used. |
expense_for | integer | null | No | Optional user id the expense is assigned to. |
additional_notes | string | null | No | Optional notes. |
expense_category_id | integer | null | No | Optional top-level expense-category id. |
expense_sub_category_id | integer | null | No | Optional child category id. Requires a matching expense_category_id. |
contact_id | integer | null | No | Optional business contact id. |
tax_id | integer | null | No | Optional business tax-rate id. |
is_refund | boolean | No | Set to true to create an expense_refund row instead of a normal expense. |
is_recurring | boolean | No | Whether the expense should recur automatically. |
recur_interval | integer | null | No | Recurring interval. Defaults to 1 when is_recurring is enabled and this field is omitted. |
recur_interval_type | string | null | No | days, months, or years. |
recur_repetitions | integer | null | No | Optional max repetition count. |
subscription_repeat_on | integer | null | No | Optional repeat day-of-month for monthly recurring expenses. |
payment | array<ExpenseCreatePaymentInput> | null | No | Optional payment rows created in the same transaction. |
document | file | null | No | Optional uploaded document when using multipart requests. |
ExpenseCreatePaymentInput object| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Payment amount. |
method | string | Yes | Payment method key. |
paid_on | string | null | No | Optional payment date string. |
note | string | null | No | Optional payment note. |
is_return | boolean | null | No | Optional return-payment flag. |
account_id | integer | null | No | Optional account id. |
| Field | Type | Description |
|---|---|---|
data | ExpenseDetail | The created expense payload reloaded from the visible detail query. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The expense was created successfully. | { "data": { ... } } |
402 | The business subscription is inactive. | { "message": string } |
403 | Demo mode is active or the token user lacks expense.add. | { "message": string } |
422 | The request body failed validation, the location is not permitted for the token user, or the create pipeline raised a business-rule error. | Laravel validation JSON or { "message": string }. |
500 | The expense was saved but could not be reloaded for the final response. | { "message": "Could not load expense" } |
Updates an existing visible expense or expense refund.
| Property | Value |
|---|---|
| Method | PUT or PATCH |
| Path | /api/v1/integration/expenses/{id} |
| Permission | expense.edit. |
| Subscription | The business must have an active subscription. |
| Demo mode | Returns 403 in demo environments. |
| Visibility | The target row is loaded through the same permitted-location and expense visibility query used by list and show. |
| Partial updates | Most create fields may be sent optionally. If location_id is sent, it must still be permitted for the token user. |
| Recurring caveat | The underlying util treats a missing or falsy is_recurring value as disabled recurrence. Clients that need to preserve recurrence should resend is_recurring = true and the related recurring fields explicitly. |
| Payment caveat | The update validator accepts payment, but the underlying expense update flow does not apply payment rows. Use Record expense payment instead. |
| Success response | 200 with one ExpenseDetail object. |
| Field | Type | Required | Description |
|---|---|---|---|
| All Add expense fields | mixed | No | The same field set can be sent on update, but all fields are optional and applied partially. |
document | file | null | No | Optional uploaded document when using multipart requests. |
is_recurring | boolean | No | Send true to keep or enable recurring behavior. Omitting the key or sending false clears recurrence in the underlying util. |
| Field | Type | Description |
|---|---|---|
data | ExpenseDetail | The updated expense payload reloaded from the visible detail query. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The expense was updated successfully. | { "data": { ... } } |
402 | The business subscription is inactive. | { "message": string } |
403 | Demo mode is active or the token user lacks expense.edit. | { "message": string } |
404 | The expense id does not exist in the visible query. | { "message": "Not found" } |
422 | The request body failed validation, the location is not permitted for the token user, or the underlying update pipeline rejected the request. | Laravel validation JSON or { "message": string }. |
500 | The expense was updated but could not be reloaded for the final response. | { "message": "Could not load expense" } |
Records a payment against a visible expense or expense refund.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/expenses/{id}/payments |
| Permission | Same access gate as List expenses: all_expense.access or view_own_expense. |
| Demo and subscription | Returns 403 in demo environments and 402 when the business subscription is inactive. |
| Payment-method rule | method must be one of the payment methods currently allowed for the expense location. |
| Advance rule | When method = advance, the payment amount cannot exceed the linked contact balance. |
| Success response | 201 with the created payment id and refreshed expense payment status. |
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Payment amount. Minimum 0.01. |
method | string | Yes | Payment method key. |
paid_on | string | null | No | Optional payment date. Defaults to the current timestamp when omitted. |
note | string | null | No | Optional payment note up to 1000 characters. |
account_id | integer | null | No | Optional account id. Ignored when method = advance. |
card_number, card_holder_name, card_transaction_number, card_type, card_month, card_year, card_security | string | null | No | Optional card-payment metadata. |
cheque_number | string | null | No | Optional cheque number. |
bank_account_number | string | null | No | Optional bank-account reference. |
transaction_no_1, transaction_no_2, transaction_no_3 | string | null | No | Optional transaction references used by custom_pay_1, custom_pay_2, and custom_pay_3. |
| Field | Type | Description |
|---|---|---|
message | string | Localized payment-added message. |
data.transaction_id, data.payment_id | integer | Expense id and created payment id. |
data.payment_ref_no | string | null | Generated payment reference number. |
data.payment_status | string | null | Refreshed expense payment status after the insert. |
data.amount | number | Stored payment amount. |
data.method | string | null | Stored payment method. |
data.paid_on | string | null | ISO-8601 payment timestamp. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The payment was recorded successfully. | { "message": string, "data": { ... } } |
402 | The business subscription is inactive. | { "message": string } |
403 | Demo mode is active or the token user lacks expense-list access. | { "message": string } |
404 | The expense id does not exist in the visible query. | { "message": "Not found" } |
422 | The expense is already fully paid, the request body failed validation, the payment method is invalid for the location, or an advance payment exceeds the contact balance. | Laravel validation JSON or { "message": string }. |
500 | The expense payment transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Deletes a visible expense or expense refund.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/v1/integration/expenses/{id} |
| Permission | expense.delete. |
| Demo mode | Returns 403 in demo environments. |
| Visibility | The target row is loaded through the same permitted-location and expense visibility query used by list and show. |
| Delete behavior | Deletes any linked cash-register payments, removes account transactions for the expense id, and dispatches the expense-modified delete event. |
| Success response | 200 with the deleted id. |
| Field | Type | Description |
|---|---|---|
message | string | Localized delete-success message. |
data.id | integer | The deleted expense id. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The expense was deleted successfully. | { "message": string, "data": { "id": integer } } |
403 | Demo mode is active or the token user lacks expense.delete. | { "message": string } |
404 | The expense id does not exist in the visible query. | { "message": "Not found" } |
500 | The expense delete transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Lists parent categories and sub-categories available to the business.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/expense-categories |
| Permission | expense.add or expense.edit. |
| Search behavior | q matches category name, code, and numeric id. Legacy search is also supported. Only q enforces the 2-character minimum. |
| Scope behavior | scope=parents returns only top-level categories. scope=children returns only sub-categories. |
| CSV behavior | format=csv streams all matching rows with a UTF-8 BOM and ignores page and per_page. parent and sub_categories are JSON-encoded in CSV cells. |
| Success response | 200 with paginated ExpenseCategoryRow rows. |
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page, page | integer | No | Pagination controls. per_page accepts 1 to 100 and defaults to 20. |
q | string | No | Primary search term. Minimum 2 characters when sent. |
search | string | No | Legacy search term with no minimum length enforcement. |
sort | string | No | name, code, or created_at. Defaults to name. |
direction | string | No | asc or desc. Defaults to asc. |
scope | string | No | parents or children. |
format | string | No | json (default) or csv. |
ExpenseCategoryRow object| Field | Type | Description |
|---|---|---|
id | integer | Expense category id. |
name, code | string | null | Stored category values. |
parent_id | integer | null | Parent category id for sub-categories. |
is_sub_category | boolean | Whether this row is a child category. |
display_name | string | Category name or Parent / Child display label. |
parent | object | null | Parent summary with id and name for child rows. |
sub_categories | array<object> | Child summaries with id, name, and code. Parent rows include their direct children; child rows usually return an empty array. |
created_at, updated_at | string | null | ISO-8601 timestamps. |
| Field | Type | Description |
|---|---|---|
data | array<ExpenseCategoryRow> | The current result page. |
meta.current_page, meta.last_page, meta.per_page, meta.total | integer | Laravel paginator metadata. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The category list was returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks expense-category access. | { "message": "Unauthorized" } |
422 | The query string failed validation or q was shorter than 2 characters. | Laravel validation JSON or { "message": string }. |
500 | The category list query failed unexpectedly. | { "message": "Could not list expense categories" } |
Returns one expense category or sub-category by id.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/expense-categories/{id} |
| Permission | expense.add or expense.edit. |
| CSV behavior | format=csv streams one UTF-8 BOM row whose data_json cell matches the JSON data payload. |
| Success response | 200 with one ExpenseCategoryRow object. |
| Field | Type | Description |
|---|---|---|
data | ExpenseCategoryRow | The requested category payload. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The category was returned successfully. | { "data": { ... } } or CSV download. |
403 | The token user lacks expense-category access. | { "message": "Unauthorized" } |
404 | The category id does not exist in the business. | { "message": "Not found" } |
422 | The query string failed validation. | Laravel validation JSON. |
Creates a parent category or a child sub-category.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/expense-categories |
| Permission | expense.add or expense.edit. |
| Demo mode | Returns 403 in demo environments. |
| Parent rule | parent_id, when sent, must point to a top-level expense category in the same business. |
| Success response | 201 with one ExpenseCategoryRow object. |
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Category name. |
code | string | null | No | Optional category code. |
parent_id | integer | null | No | Optional top-level parent category id. Omit or send null to create a parent category. |
| Field | Type | Description |
|---|---|---|
data | ExpenseCategoryRow | The created category payload. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The category was created successfully. | { "data": { ... } } |
403 | Demo mode is active or the token user lacks expense-category access. | { "message": string } |
422 | The request body failed validation or parent_id does not point to a top-level parent category. | Laravel validation JSON or { "message": "Invalid parent_id" }. |
500 | The category create query failed unexpectedly. | { "message": "something_went_wrong" } |
Updates an existing category or sub-category.
| Property | Value |
|---|---|
| Method | PUT or PATCH |
| Path | /api/v1/integration/expense-categories/{id} |
| Permission | expense.add or expense.edit. |
| Demo mode | Returns 403 in demo environments. |
| Parent rule | parent_id is required on update, even when you want the category to be top-level. Send null for top-level. |
| Self-parent rule | A category cannot be its own parent. |
| Success response | 200 with one ExpenseCategoryRow object. |
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Category name. |
code | string | null | No | Optional category code. |
parent_id | integer | null | Yes | Top-level parent category id, or null to make the category top-level. |
| Field | Type | Description |
|---|---|---|
data | ExpenseCategoryRow | The updated category payload. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The category was updated successfully. | { "data": { ... } } |
403 | Demo mode is active or the token user lacks expense-category access. | { "message": string } |
404 | The category id does not exist in the business. | { "message": "Not found" } |
422 | The request body failed validation, the category was assigned to itself, or parent_id does not point to a top-level parent category. | Laravel validation JSON or { "message": string }. |
500 | The category update query failed unexpectedly. | { "message": "something_went_wrong" } |
Soft-deletes a category and its direct child categories.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/v1/integration/expense-categories/{id} |
| Permission | expense.add or expense.edit. |
| Demo mode | Returns 403 in demo environments. |
| Delete behavior | The controller soft-deletes the target row and then soft-deletes any direct child categories with parent_id = {id}. |
| Success response | 200 with the deleted id. |
| Field | Type | Description |
|---|---|---|
message | string | Localized delete-success message. |
data.id | integer | The deleted category id. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The category was deleted successfully. | { "message": string, "data": { "id": integer } } |
403 | Demo mode is active or the token user lacks expense-category access. | { "message": string } |
404 | The category id does not exist in the business. | { "message": "Not found" } |
500 | The category delete query failed unexpectedly. | { "message": "something_went_wrong" } |
Imports expense rows from the same CSV format used by the web expense-import screen.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/import/expenses |
| Permission | expense.add. |
| Content type | multipart/form-data |
| Middleware note | This route also runs session and integration.web_context middleware because it delegates into the legacy web expense-import pipeline. |
| Import behavior | Each CSV row creates a type = expense, status = final transaction and one payment row, then recalculates payment status. |
| Auto-create behavior | Missing category and sub-category names are created automatically in the business before the expense row is inserted. |
| Success response | 200 with a success flag and localized message. |
| Field | Type | Required | Description |
|---|---|---|---|
expense_csv | file | Yes | CSV, XLS, or XLSX file using the expense-import template columns. |
| Column | Required | Description |
|---|---|---|
LOCATION | No | Business location name. When blank, the importer uses the first business location. |
CATEGORY | No | Expense category name. Missing names are created automatically. |
SUB-CATEGORY | No | Sub-category name under the chosen category. Missing names are created automatically. |
REFERENCE NO | No | Custom expense reference. When blank, the importer auto-generates an expense reference number. |
EXPENSE DATE | No | Expense date string. When blank, the importer uses the current timestamp. |
EXPENSE FOR | No | User email or username. The importer resolves it to expense_for. |
EXPENSE FOR CONTACT ID | No | Business contact identifier from the contact master data. |
ATTACH DOCUMENT | No | Optional legacy document reference string used by the import pipeline. |
APPLICABLE TAX | No | Tax-rate name. When matched, the importer back-calculates total_before_tax and tax_amount. |
EXPENSE NOTE | No | Optional expense note stored as additional_notes. |
TOTAL AMOUNT | Yes | Final expense total. |
PAID AMOUNT | Yes | Initial payment amount created for the imported expense. |
PAID ON DATE | No | Payment date string. When blank, the importer uses the current timestamp. |
PAYMENT METHOD | Yes | Displayed payment-method label from the business payment-method list. |
PAYMENT ACCOUND | No | Account number used to look up the payment account. The template header contains the same ACCOUND spelling. |
PAYMENT NOTE | No | Optional payment note stored on the payment row. |
| Field | Type | Description |
|---|---|---|
success | integer | 1 on success or 0 on import failure. |
message | string | Localized success text or the first row-level validation/import error. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The import completed successfully. | { "success": 1, "message": string } |
403 | The token user lacks expense.add. | { "message": "Unauthorized" } |
422 | The upload field was missing, the file failed validation, or any imported row failed the delegated import pipeline. | Laravel validation JSON or { "success": 0, "message": string }. |