Home Integration API reference

Integration API reference

REST API for integrations and external apps

Expenses

List expenses

Lists expense and expense_refund transactions that are visible to the token user.

Endpoint summary

PropertyValue
MethodGET
Path/api/v1/integration/expenses
Permissionall_expense.access or view_own_expense.
VisibilityRows 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 coverageReturns both type = expense and type = expense_refund rows unless filtered.
CSV behaviorformat=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 response200 with paginated ExpenseRow rows.

Query parameters

ParameterTypeRequiredDescription
per_page, pageintegerNoPagination 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_idintegerNoOptional location, contact, user, and category filters.
start_date, end_datestringNoOptional Y-m-d bounds on transaction_date. The filter is only applied when both values are present.
payment_statusstringNopaid, due, or partial.
typestringNoexpense or expense_refund.
qstringNoMinimum 2 characters when sent. Matches ref, document, notes, numeric id, linked contact fields, and category/sub-category names.
formatstringNojson (default) or csv.

ExpenseRow object

FieldTypeDescription
idintegerExpense transaction id.
typestring | nullexpense or expense_refund.
ref_no, documentstring | nullStored reference and document fields.
transaction_datestring | nullISO-8601 transaction timestamp.
payment_statusstring | nullCurrent payment state.
final_total, total_before_tax, tax_amount, total_paidnumber | nullExpense totals from the transaction header and payment subquery.
additional_notesstring | nullSaved notes.
is_recurringbooleanWhether this expense is configured as recurring.
created_byinteger | nullCreator user id.
expense_category, expense_sub_categoryobject | nullCategory summaries with id and name.
expense_forobject | nullUser summary with id and combined display name.
contactobject | nullContact summary with id, name, mobile, contact_id, and supplier_business_name.
locationobject | nullBusiness-location summary with id and name.

Top-level JSON response

FieldTypeDescription
dataarray<ExpenseRow>The current result page.
meta.current_page, meta.last_page, meta.per_page, meta.totalintegerLaravel paginator metadata.

Status codes

StatusWhen it happensResponse shape
200The expense list was returned successfully.{ "data": [...], "meta": { ... } } or CSV download.
403The token user lacks expense-list access.{ "message": string }
422The query string failed validation or q was shorter than 2 characters.Laravel validation JSON or { "message": string }.

Get expense

Returns one visible expense or expense refund with recurring metadata and payment history.

Endpoint summary

PropertyValue
MethodGET
Path/api/v1/integration/expenses/{id}
PermissionSame permission and visibility rules as List expenses.
CSV behaviorformat=csv streams one UTF-8 BOM row whose data_json column matches the JSON data payload.
Success response200 with one ExpenseDetail object.

ExpensePaymentSummary object

FieldTypeDescription
idintegerPayment-line id.
amountnumber | nullStored payment amount.
methodstring | nullPayment method key.
paid_onstring | nullISO-8601 payment timestamp.
payment_ref_nostring | nullGenerated payment reference number.
notestring | nullOptional payment note.
is_returnbooleanWhether the payment is recorded as a return/negative payment line.

ExpenseDetail object

FieldTypeDescription
id, type, ref_nointeger | stringExpense identifiers and type.
documentstring | nullStored document filename or path.
transaction_datestring | nullISO-8601 transaction timestamp.
payment_statusstring | nullCurrent payment state.
final_total, total_before_tax, tax_amount, discount_amount, total_paidnumber | nullExpense totals from the header and payment lines.
additional_notes, staff_notestring | nullSaved notes.
is_recurringbooleanWhether this expense is configured as recurring.
recur_interval, recur_repetitions, recur_parent_idinteger | nullRecurring schedule counters and parent recurring invoice id.
recur_interval_typestring | nulldays, months, or years.
subscription_repeat_oninteger | nullRepeat day-of-month value used by monthly recurring expenses.
created_byinteger | nullCreator user id.
expense_category, expense_sub_categoryobject | nullCategory summaries with id and name.
expense_forobject | nullUser summary with id, combined display name, and email.
contactobject | nullContact summary with id, name, mobile, email, contact_id, and supplier_business_name.
locationobject | nullBusiness-location summary with id and name.
order_taxobject | nullTax summary with id, name, amount, and is_tax_group.
paymentsarray<ExpensePaymentSummary>Payment lines linked to the expense.

Top-level JSON response

FieldTypeDescription
dataExpenseDetailThe requested expense payload.

Status codes

StatusWhen it happensResponse shape
200The expense was returned successfully.{ "data": { ... } } or CSV download.
403The token user lacks expense-list access.{ "message": string }
404The expense id does not exist in the visible query.{ "message": "Not found" }
422The query string failed validation.Laravel validation JSON.

Create expense

Creates a new expense or expense refund and can optionally post initial payment lines in the same request.

Endpoint summary

PropertyValue
MethodPOST
Path/api/v1/integration/expenses
Permissionexpense.add.
SubscriptionThe business must have an active subscription.
Demo modeReturns 403 in demo environments.
Location rulelocation_id must belong to the business and must be included in the token user's permitted locations.
Type behavioris_refund = true creates type = expense_refund; otherwise the controller creates type = expense. The transaction is stored with status = final.
Recurring behaviorWhen is_recurring is truthy, the create pipeline stores recurring fields and defaults recur_interval to 1 when omitted.
Upload behaviorThe 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 response201 with one ExpenseDetail object.

Request body

FieldTypeRequiredDescription
location_idintegerYesBusiness location id.
final_totalnumberYesTotal expense amount including tax.
ref_nostring | nullNoOptional expense reference number. Auto-generated when omitted.
transaction_datestring | nullNoOptional date string passed through the same date-formatting pipeline as the web expense form. When omitted, the current timestamp is used.
expense_forinteger | nullNoOptional user id the expense is assigned to.
additional_notesstring | nullNoOptional notes.
expense_category_idinteger | nullNoOptional top-level expense-category id.
expense_sub_category_idinteger | nullNoOptional child category id. Requires a matching expense_category_id.
contact_idinteger | nullNoOptional business contact id.
tax_idinteger | nullNoOptional business tax-rate id.
is_refundbooleanNoSet to true to create an expense_refund row instead of a normal expense.
is_recurringbooleanNoWhether the expense should recur automatically.
recur_intervalinteger | nullNoRecurring interval. Defaults to 1 when is_recurring is enabled and this field is omitted.
recur_interval_typestring | nullNodays, months, or years.
recur_repetitionsinteger | nullNoOptional max repetition count.
subscription_repeat_oninteger | nullNoOptional repeat day-of-month for monthly recurring expenses.
paymentarray<ExpenseCreatePaymentInput> | nullNoOptional payment rows created in the same transaction.
documentfile | nullNoOptional uploaded document when using multipart requests.

ExpenseCreatePaymentInput object

FieldTypeRequiredDescription
amountnumberYesPayment amount.
methodstringYesPayment method key.
paid_onstring | nullNoOptional payment date string.
notestring | nullNoOptional payment note.
is_returnboolean | nullNoOptional return-payment flag.
account_idinteger | nullNoOptional account id.

Top-level JSON response

FieldTypeDescription
dataExpenseDetailThe created expense payload reloaded from the visible detail query.

Status codes

StatusWhen it happensResponse shape
201The expense was created successfully.{ "data": { ... } }
402The business subscription is inactive.{ "message": string }
403Demo mode is active or the token user lacks expense.add.{ "message": string }
422The 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 }.
500The expense was saved but could not be reloaded for the final response.{ "message": "Could not load expense" }

Update expense

Updates an existing visible expense or expense refund.

Endpoint summary

PropertyValue
MethodPUT or PATCH
Path/api/v1/integration/expenses/{id}
Permissionexpense.edit.
SubscriptionThe business must have an active subscription.
Demo modeReturns 403 in demo environments.
VisibilityThe target row is loaded through the same permitted-location and expense visibility query used by list and show.
Partial updatesMost create fields may be sent optionally. If location_id is sent, it must still be permitted for the token user.
Recurring caveatThe 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 caveatThe update validator accepts payment, but the underlying expense update flow does not apply payment rows. Use Record expense payment instead.
Success response200 with one ExpenseDetail object.

Request body

FieldTypeRequiredDescription
All Add expense fieldsmixedNoThe same field set can be sent on update, but all fields are optional and applied partially.
documentfile | nullNoOptional uploaded document when using multipart requests.
is_recurringbooleanNoSend true to keep or enable recurring behavior. Omitting the key or sending false clears recurrence in the underlying util.

Top-level JSON response

FieldTypeDescription
dataExpenseDetailThe updated expense payload reloaded from the visible detail query.

Status codes

StatusWhen it happensResponse shape
200The expense was updated successfully.{ "data": { ... } }
402The business subscription is inactive.{ "message": string }
403Demo mode is active or the token user lacks expense.edit.{ "message": string }
404The expense id does not exist in the visible query.{ "message": "Not found" }
422The 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 }.
500The expense was updated but could not be reloaded for the final response.{ "message": "Could not load expense" }

Record expense payment

Records a payment against a visible expense or expense refund.

Endpoint summary

PropertyValue
MethodPOST
Path/api/v1/integration/expenses/{id}/payments
PermissionSame access gate as List expenses: all_expense.access or view_own_expense.
Demo and subscriptionReturns 403 in demo environments and 402 when the business subscription is inactive.
Payment-method rulemethod must be one of the payment methods currently allowed for the expense location.
Advance ruleWhen method = advance, the payment amount cannot exceed the linked contact balance.
Success response201 with the created payment id and refreshed expense payment status.

Request body

FieldTypeRequiredDescription
amountnumberYesPayment amount. Minimum 0.01.
methodstringYesPayment method key.
paid_onstring | nullNoOptional payment date. Defaults to the current timestamp when omitted.
notestring | nullNoOptional payment note up to 1000 characters.
account_idinteger | nullNoOptional account id. Ignored when method = advance.
card_number, card_holder_name, card_transaction_number, card_type, card_month, card_year, card_securitystring | nullNoOptional card-payment metadata.
cheque_numberstring | nullNoOptional cheque number.
bank_account_numberstring | nullNoOptional bank-account reference.
transaction_no_1, transaction_no_2, transaction_no_3string | nullNoOptional transaction references used by custom_pay_1, custom_pay_2, and custom_pay_3.

Top-level JSON response

FieldTypeDescription
messagestringLocalized payment-added message.
data.transaction_id, data.payment_idintegerExpense id and created payment id.
data.payment_ref_nostring | nullGenerated payment reference number.
data.payment_statusstring | nullRefreshed expense payment status after the insert.
data.amountnumberStored payment amount.
data.methodstring | nullStored payment method.
data.paid_onstring | nullISO-8601 payment timestamp.

Status codes

StatusWhen it happensResponse shape
201The payment was recorded successfully.{ "message": string, "data": { ... } }
402The business subscription is inactive.{ "message": string }
403Demo mode is active or the token user lacks expense-list access.{ "message": string }
404The expense id does not exist in the visible query.{ "message": "Not found" }
422The 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 }.
500The expense payment transaction failed unexpectedly.{ "message": "something_went_wrong" }

Delete expense

Deletes a visible expense or expense refund.

Endpoint summary

PropertyValue
MethodDELETE
Path/api/v1/integration/expenses/{id}
Permissionexpense.delete.
Demo modeReturns 403 in demo environments.
VisibilityThe target row is loaded through the same permitted-location and expense visibility query used by list and show.
Delete behaviorDeletes any linked cash-register payments, removes account transactions for the expense id, and dispatches the expense-modified delete event.
Success response200 with the deleted id.

Top-level JSON response

FieldTypeDescription
messagestringLocalized delete-success message.
data.idintegerThe deleted expense id.

Status codes

StatusWhen it happensResponse shape
200The expense was deleted successfully.{ "message": string, "data": { "id": integer } }
403Demo mode is active or the token user lacks expense.delete.{ "message": string }
404The expense id does not exist in the visible query.{ "message": "Not found" }
500The expense delete transaction failed unexpectedly.{ "message": "something_went_wrong" }

List expense categories

Lists parent categories and sub-categories available to the business.

Endpoint summary

PropertyValue
MethodGET
Path/api/v1/integration/expense-categories
Permissionexpense.add or expense.edit.
Search behaviorq matches category name, code, and numeric id. Legacy search is also supported. Only q enforces the 2-character minimum.
Scope behaviorscope=parents returns only top-level categories. scope=children returns only sub-categories.
CSV behaviorformat=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 response200 with paginated ExpenseCategoryRow rows.

Query parameters

ParameterTypeRequiredDescription
per_page, pageintegerNoPagination controls. per_page accepts 1 to 100 and defaults to 20.
qstringNoPrimary search term. Minimum 2 characters when sent.
searchstringNoLegacy search term with no minimum length enforcement.
sortstringNoname, code, or created_at. Defaults to name.
directionstringNoasc or desc. Defaults to asc.
scopestringNoparents or children.
formatstringNojson (default) or csv.

ExpenseCategoryRow object

FieldTypeDescription
idintegerExpense category id.
name, codestring | nullStored category values.
parent_idinteger | nullParent category id for sub-categories.
is_sub_categorybooleanWhether this row is a child category.
display_namestringCategory name or Parent / Child display label.
parentobject | nullParent summary with id and name for child rows.
sub_categoriesarray<object>Child summaries with id, name, and code. Parent rows include their direct children; child rows usually return an empty array.
created_at, updated_atstring | nullISO-8601 timestamps.

Top-level JSON response

FieldTypeDescription
dataarray<ExpenseCategoryRow>The current result page.
meta.current_page, meta.last_page, meta.per_page, meta.totalintegerLaravel paginator metadata.

Status codes

StatusWhen it happensResponse shape
200The category list was returned successfully.{ "data": [...], "meta": { ... } } or CSV download.
403The token user lacks expense-category access.{ "message": "Unauthorized" }
422The query string failed validation or q was shorter than 2 characters.Laravel validation JSON or { "message": string }.
500The category list query failed unexpectedly.{ "message": "Could not list expense categories" }

Get expense category

Returns one expense category or sub-category by id.

Endpoint summary

PropertyValue
MethodGET
Path/api/v1/integration/expense-categories/{id}
Permissionexpense.add or expense.edit.
CSV behaviorformat=csv streams one UTF-8 BOM row whose data_json cell matches the JSON data payload.
Success response200 with one ExpenseCategoryRow object.

Top-level JSON response

FieldTypeDescription
dataExpenseCategoryRowThe requested category payload.

Status codes

StatusWhen it happensResponse shape
200The category was returned successfully.{ "data": { ... } } or CSV download.
403The token user lacks expense-category access.{ "message": "Unauthorized" }
404The category id does not exist in the business.{ "message": "Not found" }
422The query string failed validation.Laravel validation JSON.

Create expense category

Creates a parent category or a child sub-category.

Endpoint summary

PropertyValue
MethodPOST
Path/api/v1/integration/expense-categories
Permissionexpense.add or expense.edit.
Demo modeReturns 403 in demo environments.
Parent ruleparent_id, when sent, must point to a top-level expense category in the same business.
Success response201 with one ExpenseCategoryRow object.

Request body

FieldTypeRequiredDescription
namestringYesCategory name.
codestring | nullNoOptional category code.
parent_idinteger | nullNoOptional top-level parent category id. Omit or send null to create a parent category.

Top-level JSON response

FieldTypeDescription
dataExpenseCategoryRowThe created category payload.

Status codes

StatusWhen it happensResponse shape
201The category was created successfully.{ "data": { ... } }
403Demo mode is active or the token user lacks expense-category access.{ "message": string }
422The request body failed validation or parent_id does not point to a top-level parent category.Laravel validation JSON or { "message": "Invalid parent_id" }.
500The category create query failed unexpectedly.{ "message": "something_went_wrong" }

Update expense category

Updates an existing category or sub-category.

Endpoint summary

PropertyValue
MethodPUT or PATCH
Path/api/v1/integration/expense-categories/{id}
Permissionexpense.add or expense.edit.
Demo modeReturns 403 in demo environments.
Parent ruleparent_id is required on update, even when you want the category to be top-level. Send null for top-level.
Self-parent ruleA category cannot be its own parent.
Success response200 with one ExpenseCategoryRow object.

Request body

FieldTypeRequiredDescription
namestringYesCategory name.
codestring | nullNoOptional category code.
parent_idinteger | nullYesTop-level parent category id, or null to make the category top-level.

Top-level JSON response

FieldTypeDescription
dataExpenseCategoryRowThe updated category payload.

Status codes

StatusWhen it happensResponse shape
200The category was updated successfully.{ "data": { ... } }
403Demo mode is active or the token user lacks expense-category access.{ "message": string }
404The category id does not exist in the business.{ "message": "Not found" }
422The 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 }.
500The category update query failed unexpectedly.{ "message": "something_went_wrong" }

Delete expense category

Soft-deletes a category and its direct child categories.

Endpoint summary

PropertyValue
MethodDELETE
Path/api/v1/integration/expense-categories/{id}
Permissionexpense.add or expense.edit.
Demo modeReturns 403 in demo environments.
Delete behaviorThe controller soft-deletes the target row and then soft-deletes any direct child categories with parent_id = {id}.
Success response200 with the deleted id.

Top-level JSON response

FieldTypeDescription
messagestringLocalized delete-success message.
data.idintegerThe deleted category id.

Status codes

StatusWhen it happensResponse shape
200The category was deleted successfully.{ "message": string, "data": { "id": integer } }
403Demo mode is active or the token user lacks expense-category access.{ "message": string }
404The category id does not exist in the business.{ "message": "Not found" }
500The category delete query failed unexpectedly.{ "message": "something_went_wrong" }

Import expenses CSV (POST multipart)

Imports expense rows from the same CSV format used by the web expense-import screen.

Endpoint summary

PropertyValue
MethodPOST
Path/api/v1/integration/import/expenses
Permissionexpense.add.
Content typemultipart/form-data
Middleware noteThis route also runs session and integration.web_context middleware because it delegates into the legacy web expense-import pipeline.
Import behaviorEach CSV row creates a type = expense, status = final transaction and one payment row, then recalculates payment status.
Auto-create behaviorMissing category and sub-category names are created automatically in the business before the expense row is inserted.
Success response200 with a success flag and localized message.

Multipart fields

FieldTypeRequiredDescription
expense_csvfileYesCSV, XLS, or XLSX file using the expense-import template columns.

CSV columns

ColumnRequiredDescription
LOCATIONNoBusiness location name. When blank, the importer uses the first business location.
CATEGORYNoExpense category name. Missing names are created automatically.
SUB-CATEGORYNoSub-category name under the chosen category. Missing names are created automatically.
REFERENCE NONoCustom expense reference. When blank, the importer auto-generates an expense reference number.
EXPENSE DATENoExpense date string. When blank, the importer uses the current timestamp.
EXPENSE FORNoUser email or username. The importer resolves it to expense_for.
EXPENSE FOR CONTACT IDNoBusiness contact identifier from the contact master data.
ATTACH DOCUMENTNoOptional legacy document reference string used by the import pipeline.
APPLICABLE TAXNoTax-rate name. When matched, the importer back-calculates total_before_tax and tax_amount.
EXPENSE NOTENoOptional expense note stored as additional_notes.
TOTAL AMOUNTYesFinal expense total.
PAID AMOUNTYesInitial payment amount created for the imported expense.
PAID ON DATENoPayment date string. When blank, the importer uses the current timestamp.
PAYMENT METHODYesDisplayed payment-method label from the business payment-method list.
PAYMENT ACCOUNDNoAccount number used to look up the payment account. The template header contains the same ACCOUND spelling.
PAYMENT NOTENoOptional payment note stored on the payment row.

Top-level JSON response

FieldTypeDescription
successinteger1 on success or 0 on import failure.
messagestringLocalized success text or the first row-level validation/import error.

Status codes

StatusWhen it happensResponse shape
200The import completed successfully.{ "success": 1, "message": string }
403The token user lacks expense.add.{ "message": "Unauthorized" }
422The 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 }.