Home Integration API reference
REST API for integrations and external apps
Sales
Lists finalized sales visible to the authenticated user across permitted locations. This is the same data source used by the web All Sales screen.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/sells |
| Authentication | Bearer token or API key + secret required. |
| Permission | Admin access or any visibility path accepted by userCanListSells, including sell view, direct-sell, own-sell, commission-agent, shipment, or sales-order view permissions. |
| Row scope | Only type = sell, status = final rows are returned. sub_type = project_invoice is excluded. |
| Visibility rules | Permitted-location scope applies. When the user lacks direct_sell.view, the list is restricted to their own sales and/or commission-agent sales based on their permissions. Non-admin users can also be narrowed by payment-status-only permissions. |
| CSV behavior | format=csv streams all matching rows and ignores pagination. Nested contact and location objects are JSON-encoded in cells. |
| Success response | 200 with paginated SellListRow rows. |
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page | integer | No | Results per page from 1 to 100. Defaults to 20. |
page | integer | No | Pagination page number. |
location_id | integer | No | Optional business location filter, still limited by permitted locations. |
contact_id | integer | No | Optional customer filter. |
created_by | integer | No | Optional creator user id filter. |
start_date, end_date | string | No | Optional transaction date range in Y-m-d. The filter is applied only when both are present. |
payment_status | string | No | paid, due, partial, or overdue. The overdue filter uses pay-term calculations. |
only_shipments | boolean-like string | No | When true, only rows with a non-null shipping_status are returned. Users with access_pending_shipments_only will not see delivered rows. |
is_direct_sale | integer | No | 1 for direct sales only; 0 for POS sales only, which also forces sub_type to null. |
q | string | No | Minimum 2 characters when sent. Matches ref_no, invoice_no, numeric transaction id, or linked contact name, business name, mobile, or contact code. |
format | string | No | json (default) or csv. |
SellListRow object| Field | Type | Description |
|---|---|---|
id | integer | Sale transaction id. |
invoice_no | string | null | Invoice number. |
transaction_date | string | null | ISO-8601 transaction timestamp. |
status, payment_status | string | null | Finalized transaction status and current payment status. |
final_total, total_before_tax, tax_amount, discount_amount, total_paid | number | null | Header totals for the sale. |
is_direct_sale | boolean | Whether the row is a direct sale. |
sub_type | string | null | Sale sub-type when present. |
consumption_type | string | Outbound consumption classification for the sale: sales, damages, or sampling (defaults to sales when unset on older rows). |
contact | object | null | Contact summary with id, name, mobile, contact_id, and supplier_business_name. |
location | object | null | Location summary with id and name. |
| Field | Type | Description |
|---|---|---|
data | array<SellListRow> | Paginated finalized sale rows. |
meta.current_page, meta.last_page, meta.per_page, meta.total | integer | Pagination metadata. |
| Status | When it happens | Response shape |
|---|---|---|
200 | Sales were returned successfully. | JSON { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user does not pass the sell list permission gate. | { "message": "Unauthorized" } |
422 | The query string failed validation, such as a 1-character q. | Laravel validation JSON or { "message": string }. |
Checks whether an invoice number is available in the current business before creating or editing a sale.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/sells/check-invoice-number |
| Permission | The token user must be allowed to create either direct or POS sales: sell.create, direct_sell.access, or so.create. |
| Success response | 200 with an availability boolean and the checked invoice number. |
| Parameter | Type | Required | Description |
|---|---|---|---|
invoice_no | string | Yes | Invoice number to test for uniqueness in the current business. |
exclude_transaction_id | integer | No | Optional transaction id to ignore, useful when editing an existing sale and keeping the same invoice number. |
| Field | Type | Description |
|---|---|---|
data.available | boolean | true when no other transaction in the business uses the invoice number after applying the optional exclusion. |
data.invoice_no | string | Echoed invoice number that was checked. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The invoice number check completed successfully. | { "data": { "available": boolean, "invoice_no": string } } |
403 | The token user cannot create sales and therefore cannot use this availability check. | { "message": "Unauthorized" } |
422 | The query string failed validation. | Laravel validation JSON. |
Returns one finalized sale with header totals, line items, and payment summaries.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/sells/{id} |
| Permission | Same permission gate and visibility rules as List sells. |
| CSV behavior | format=csv streams one row with a data_json column containing the full JSON payload. |
| Success response | 200 with a SellDetail object. |
| Parameter | Type | Required | Description |
|---|---|---|---|
format | string | No | json (default) or csv. |
SellLine object| Field | Type | Description |
|---|---|---|
id, product_id, variation_id | integer | Sell line ids. |
product_label | string | Formatted product and variation label used in the UI. |
sub_sku | string | null | Variation SKU. |
quantity | number | Sold quantity. |
unit_price, unit_price_inc_tax, unit_price_before_discount, item_tax, line_discount_amount | number | null | Line pricing values. |
line_discount_type | string | null | fixed, percentage, or null. |
line_tax | object | null | Tax summary with id, name, and amount. |
modifiers | array<SellLine> | null | Modifier rows are only included when the sale line has modifiers. |
SellPaymentSummary object| Field | Type | Description |
|---|---|---|
id | integer | Payment line id. |
amount | number | null | Payment amount. |
method | string | null | Stored payment method key. |
paid_on | string | null | ISO-8601 payment timestamp. |
payment_ref_no | string | null | Payment reference number. |
note | string | null | Saved payment note. |
is_return | boolean | Whether the payment line is a return payment. |
SellDetail object| Field | Type | Description |
|---|---|---|
id, invoice_no, ref_no | integer | string | null | Core sale identifiers. |
transaction_date | string | null | ISO-8601 transaction timestamp. |
status, payment_status | string | null | Sale status and payment status. |
final_total, total_before_tax, tax_amount, discount_amount, shipping_charges, total_paid | number | null | Header totals. |
shipping_status | string | null | Shipping workflow status when present. |
additional_notes, staff_note | string | null | Saved sale notes. |
is_direct_sale | boolean | Whether the row is a direct sale. |
sub_type | string | null | Sale sub-type when present. |
consumption_type | string | Outbound consumption classification: sales, damages, or sampling (defaults to sales when unset on older rows). |
contact | object | null | Contact summary with id, name, mobile, email, contact_id, and supplier_business_name. |
location | object | null | Location summary with id and name. |
order_tax | object | null | Order-level tax summary with id, name, and amount. |
lines | array<SellLine> | Sell lines in display order. |
payments | array<SellPaymentSummary> | Header payment summary rows. |
| Field | Type | Description |
|---|---|---|
data | SellDetail | Detailed finalized sale payload. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The sale was returned successfully. | { "data": SellDetail } or CSV download. |
403 | The token user does not pass the sell list permission gate. | { "message": "Unauthorized" } |
404 | The sale id is not visible in the finalized sales query for the current user. | { "message": "Not found" } |
Records a payment on a finalized sale. This endpoint uses the same sell-payment pipeline as the web add-payment flow and does not post cash-register lines.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/sells/{id}/payments |
| Permission | sell.payments plus visibility to the sale through the finalized sell query. |
| Demo mode | Returns 403 in demo environments. |
| Subscription rule | Returns 402 when the business is not subscribed. |
| Held-sale rule | Suspended POS sales cannot receive payments here; finalize them through the suspended POS endpoint first. |
| Request encoding | application/json |
| Success response | 201 with a payment summary object. |
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer YOUR_ACCESS_TOKEN |
Content-Type | Yes | application/json |
Accept | No | Recommended: application/json. |
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Payment amount. Must be at least 0.01. |
method | string | Yes | Payment method key enabled for the sale location. |
paid_on | string | null | No | Optional payment date/datetime. Defaults to the current time when omitted. |
note | string | null | No | Optional payment note. |
account_id | integer | null | No | Optional account id. It is ignored for advance payments. |
card_number, card_holder_name, card_transaction_number, card_type, card_month, card_year, card_security | string | null | No | Card-payment metadata. |
cheque_number, bank_account_number | string | null | No | Cheque or bank reference data. |
transaction_no_1 to transaction_no_3 | string | null | No | Reference values for custom payment methods. |
| Field | Type | Description |
|---|---|---|
message | string | Localized success message. |
data.transaction_id | integer | Finalized sale id from the path. |
data.payment_id | integer | Created payment row id. |
data.payment_ref_no | string | null | Generated payment reference number. |
data.payment_status | string | null | Updated sale payment status after the payment is posted. |
data.amount | number | Stored payment amount. |
data.method | string | Stored payment method key. |
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 is not subscribed. | { "message": string } |
403 | Demo mode is active, the token user lacks sell.payments, or the sale is not visible. | { "message": string } |
404 | The sale id is not visible in the finalized sales query for the current user. | { "message": "Not found" } |
422 | The sale is suspended, already fully paid, the payment method is invalid for the location, or an advance payment exceeds the contact's advance balance. | Laravel validation JSON or { "message": string }. |
500 | The payment transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Duplicates a finalized sale into a new draft transaction with copied sell lines and a new draft invoice number.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/sells/{id}/duplicate |
| Permission | sell.create plus visibility to the source sale through the finalized sell query. |
| Demo mode | Returns 403 in demo environments. |
| Subscription rule | Returns 402 when the business is not subscribed. |
| Copy rules | The controller copies transaction header fields and sell lines, but clears identifiers like id, timestamps, payment_status, invoice_token, and lot_no_line_id. |
| Success response | 201 with the new draft id and invoice number. |
| Field | Type | Description |
|---|---|---|
message | string | Localized duplicate success message. |
data.id | integer | New draft transaction id. |
data.invoice_no | string | null | New draft invoice number. |
data.status | string | Always draft. |
data.is_direct_sale | boolean | Copied direct-sale flag from the source transaction. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The duplicate draft was created successfully. | { "message": string, "data": { ... } } |
402 | The business is not subscribed. | { "message": string } |
403 | Demo mode is active, the token user lacks sell.create, or the source sale is not visible. | { "message": string } |
404 | The source sale id is not visible in the finalized sales query. | { "message": "Not found" } |
500 | The duplicate transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Returns the public invoice and payment links used by the app for a finalized sale. If the invoice token is missing, the controller generates and stores one before returning the payload.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/sells/{id}/public-links |
| Permission | Same permission gate and visibility rules as List sells. |
| Subscription rule | No subscription gate beyond normal sale visibility. |
| Success response | 200 with invoice and payment URLs. |
| Field | Type | Description |
|---|---|---|
data.invoice_url | string | Tokenized customer-facing invoice URL. |
data.payment_link | string | null | Tokenized online payment link when one is available for the invoice. |
data.invoice_token | string | null | Persisted invoice token after the controller refreshes the transaction. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The public links were returned successfully. | { "data": { "invoice_url": string, "payment_link": string | null, "invoice_token": string | null } } |
403 | The token user does not pass the sell list permission gate. | { "message": "Unauthorized" } |
404 | The sale id is not visible in the finalized sales query for the current user. | { "message": "Not found" } |
Deletes a finalized sale using the same core delete pipeline as the web app.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/v1/integration/sells/{id} |
| Permission | At least one of sell.delete, direct_sell.delete, or so.delete. |
| Demo mode | Returns 403 in demo environments. |
| Subscription rule | Returns 402 when the business is not subscribed. |
| Visibility rules | The sale must still be visible through the finalized sell query for the authenticated user. |
| Delete behavior | Delegates to TransactionUtil::deleteSale, so stock reversal, purchase/sell unmapping, payment cleanup, register cleanup, return guards, and ZATCA or sync blockers are enforced exactly as they are in the app. |
| Success response | 200 with the deleted sale id. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The sale was deleted successfully. | { "message": string, "data": { "id": integer } } |
402 | The business is not subscribed. | { "message": string } |
403 | Demo mode is active or the token user lacks delete permission. | { "message": string } |
404 | The sale id is not visible in the finalized sales query for the current user. | { "message": "Not found" } |
422 | deleteSale rejected the delete because of a business rule such as linked returns or external compliance constraints. | { "message": string } |
500 | The delete transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Returns one payment line by transaction_payments.id. The endpoint supports both invoice-linked payments and standalone contact payments such as advance rows.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/transaction-payments/{id} |
| Authentication | Bearer token or API key + secret required. |
| Visibility rules | Invoice-linked rows use the same visibility checks as the parent integration transaction. Standalone contact payments require contact-payment visibility and contact access. |
| CSV behavior | format=csv streams one row with a data_json column containing the full JSON payload. |
| Success response | 200 with a TransactionPaymentDetail object. |
| Parameter | Type | Required | Description |
|---|---|---|---|
format | string | No | json (default) or csv. |
TransactionPaymentNode object| Field | Type | Description |
|---|---|---|
id | integer | Payment line id. |
amount | number | Payment amount. |
method, method_label | string | Stored payment method key and resolved label. |
payment_ref_no | string | null | Payment reference number. |
paid_on | string | null | ISO-8601 payment timestamp. |
TransactionPaymentDetail object| Field | Type | Description |
|---|---|---|
id, parent_id, payment_for | integer | null | Core payment identifiers. |
amount | number | Payment amount. |
is_return | boolean | Whether the payment line is recorded as a return. |
method, method_label | string | Stored payment method key and resolved label. |
paid_on | string | null | ISO-8601 payment timestamp. |
payment_ref_no, transaction_no, note | string | null | Saved payment references and note. |
account_id | integer | null | Linked account id. |
card_number, card_holder_name, card_transaction_number, card_type, card_month, card_year, cheque_number, bank_account_number | string | null | Stored card, cheque, and bank metadata. |
document | object | null | Uploaded document summary with name and url. |
parent_payment | TransactionPaymentNode | null | Parent payment when the row is part of a split or adjustment. |
child_payments | array<TransactionPaymentNode> | Loaded child payment rows for split payments. |
transaction_id | integer | null | Parent transaction id for invoice-linked rows; null for standalone contact payments. |
transaction | object | null | When invoice-linked: id, type, invoice_no, ref_no, and payment_status. |
contact | object | null | When standalone: id, name, type, contact_id, and supplier_business_name. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The payment line was returned successfully. | { "data": TransactionPaymentDetail } or CSV download. |
403 | The token user lacks permission to view the parent transaction or standalone contact payment. | { "message": "Unauthorized" } |
404 | The payment line does not exist, its parent transaction is not visible, or a standalone payment is missing contact linkage. | { "message": "Not found" } |
Updates an existing invoice-linked payment. Standalone advance-style rows are intentionally rejected by this endpoint.
| Property | Value |
|---|---|
| Method | PUT or PATCH |
| Path | /api/v1/integration/transaction-payments/{id} |
| Permission | edit_sell_payment for sell and sell-return parents, edit_purchase_payment for purchase parents, or expense access permissions for expense parents. |
| Demo mode | Returns 403 in demo environments. |
| Supported rows | Only rows with a non-null transaction_id. Payments whose stored method is advance are rejected. |
| Request encoding | application/json or multipart/form-data when uploading document. |
| Success response | 200 with updated payment metadata. |
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Updated payment amount. Must be at least 0.01. |
method | string | Yes | Payment method key that must be enabled for the parent transaction location. |
paid_on | string | null | No | Optional payment date/datetime. Defaults to now when omitted. |
note | string | null | No | Optional note. |
account_id | integer | null | No | Optional account id. |
card_number, card_holder_name, card_transaction_number, card_type, card_month, card_year, card_security | string | null | No | Card-payment metadata. |
cheque_number, bank_account_number | string | null | No | Cheque or bank reference data. |
transaction_no_1 to transaction_no_3 | string | null | No | Custom payment-method references. |
denominations | array | null | No | Optional cash denomination payload for installations that use denomination tracking. |
document | file | null | No | Optional uploaded document. The configured MIME list and size limit from config/constants.php are enforced. |
| Field | Type | Description |
|---|---|---|
message | string | Localized update success message. |
data.id | integer | Updated payment id. |
data.transaction_id | integer | Parent transaction id. |
data.payment_ref_no | string | null | Payment reference number. |
data.payment_status | string | null | Updated parent transaction payment status. |
data.amount | number | Stored amount after the update. |
data.method | string | Stored method key after the update. |
data.paid_on | string | null | ISO-8601 payment timestamp. |
data.document | object | null | Included only when a document is stored, with name and url. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The payment was updated successfully. | { "message": string, "data": { ... } } |
403 | Demo mode is active or the token user lacks permission for the parent transaction type. | { "message": string } |
404 | The payment row does not exist or the parent transaction is not visible. | { "message": "Not found" } |
422 | The row is standalone, the stored method is advance, the requested method is invalid for the location, the upload is invalid, or validation failed. | Laravel validation JSON or { "message": string }. |
500 | The payment update transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Deletes an invoice-linked payment row by id.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/v1/integration/transaction-payments/{id} |
| Permission | delete_sell_payment for sell and sell-return parents, delete_purchase_payment for purchase parents, or expense access permissions for expense parents. |
| Demo mode | Returns 403 in demo environments. |
| Supported rows | Only rows with a non-null transaction_id. |
| Success response | 200 with the deleted payment id and refreshed parent payment status. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The payment row was deleted successfully. | { "message": string, "data": { "id": integer, "transaction_id": integer, "payment_status": string | null } } |
403 | Demo mode is active or the token user lacks delete permission for the parent transaction type. | { "message": string } |
404 | The payment row does not exist or the parent transaction is not visible. | { "message": "Not found" } |
422 | The payment row is standalone and cannot be deleted by this endpoint. | { "message": string } |
500 | The delete transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Creates a direct sale through the Add Sale workflow. This endpoint supports draft, quotation-style, proforma, and final direct-sale creation without requiring an open cash register.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/sells |
| Authentication | Bearer token or API key + secret required. |
| Permission | sell.create or direct_sell.access. |
| Module requirement | The business must have the add_sale module enabled. |
| Subscription and quota | The business must be subscribed and still have invoice quota available. |
| Mode | Creates direct sales only. POS-specific register handling does not apply here. |
| Request encoding | application/json |
| Success response | 201 with the created sale id, invoice number, status, payment status, and direct-sale flags. |
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer YOUR_ACCESS_TOKEN |
Content-Type | Yes | application/json |
Accept | No | Recommended: application/json. |
| Field | Type | Required | Description |
|---|---|---|---|
status | string | Yes | draft or final. |
draft_kind | string | null | No | For drafts only: standard, quotation, or proforma. Defaults to standard when omitted on drafts. |
contact_id | integer | Yes | Must belong to a customer or both contact in the business. |
location_id | integer | Yes | Business location id. The authenticated user must have access to it. |
transaction_date | string | Yes | Any parseable date/datetime accepted by Laravel validation. |
tax_rate_id | integer | null | No | Optional order tax rate id from the same business. |
discount_type, discount_amount | string | number | null | No | Optional order-level discount. Type accepts fixed or percentage. |
sale_note, staff_note | string | null | No | Optional saved notes. |
shipping_details, shipping_address, shipping_status | string | null | No | Optional shipping metadata. |
shipping_charges, exchange_rate | number | null | No | Optional shipping and exchange-rate values. |
commission_agent, selling_price_group_id, invoice_scheme_id | integer | null | No | Optional direct-sale header links. |
consumption_type | string | null | No | Outbound consumption classification for integrations: sales, damages, or sampling. Omitted or invalid values default to sales. |
sales_order_ids | array<integer> | null | No | Optional linked sales-order ids. Every id must resolve to a business sales order. |
is_credit_sale | boolean | No | When truthy on a final sale, payment lines are not required at creation time. |
payments | array | null | No | Optional payment lines for final non-credit sales. Each row uses the FinalPaymentLine shape below. |
change_return | number | null | No | Optional positive cash change row for final non-credit sales. |
rp_redeemed | number | null | No | Optional reward-points redemption amount. |
products | array | Yes | At least one sale line using the CreateSellLine shape below. |
types_of_service_id, packing_charge, packing_charge_type, service_custom_field_1 to service_custom_field_6 | mixed | No | Optional only when the types_of_service module is enabled. |
CreateSellLine object| Field | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | Product id in the current business. |
variation_id | integer | Yes | Variation id for the selected product. |
quantity | number | Yes | Requested quantity. |
unit_price | number | Yes | Unit price before tax. |
unit_price_inc_tax | number | Yes | Unit price including tax. |
item_tax | number | Yes | Line tax amount. |
line_discount_type, line_discount_amount | string | number | null | No | Optional line discount details. |
FinalPaymentLine object| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Payment amount. |
method | string | Yes | Payment method key enabled for the selected location. |
paid_on | string | null | No | Optional payment date/datetime. |
account_id | integer | null | No | Optional account id. |
note | string | null | No | Optional payment note. |
is_return | boolean | No | Optional return flag; usually omitted for normal sale creation. |
| Field | Type | Description |
|---|---|---|
message | string | Localized create success message. |
data.id | integer | Created sale id. |
data.invoice_no | string | null | Generated invoice number. |
data.status | string | Stored status. |
data.payment_status | string | null | Stored payment status after the create flow completes. |
data.is_direct_sale | boolean | Always true for this endpoint. |
data.is_suspend | boolean | Always false for this endpoint. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The direct sale was created successfully. | { "message": string, "data": { ... } } |
402 | The business is not subscribed or has reached invoice quota. | { "message": string } |
403 | Demo mode is active, the token user lacks create permission, the Add Sale module is disabled, or the selected location is outside the user's permitted locations. | { "message": string } |
422 | Validation failed, the invoice total could not be calculated, the payment method is invalid for the location, a sales-order id is invalid, the customer credit limit is exceeded, a combo product has no configured components, or stock/payment business rules reject the sale. | Laravel validation JSON or { "message": string }. |
500 | The sale create transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Lists Add Sale drafts, including standard drafts, quotation drafts, and proforma drafts, with the same split visibility rules used by the web draft datatable.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/sell-drafts |
| Permission | At least one of draft.view_all, draft.view_own, quotation.view_all, or quotation.view_own. |
| Module requirement | The business must have the add_sale module enabled. |
| Visibility rules | Rows are status = draft. Quotation rows use quotation permissions; standard and proforma rows use draft permissions. Permitted-location scope applies to all rows. |
| CSV behavior | format=csv streams all matching rows and ignores pagination. |
| Success response | 200 with paginated SellDraftListRow rows. |
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page | integer | No | Results per page from 1 to 100. Defaults to 20. |
page | integer | No | Pagination page number. |
location_id, contact_id, created_by | integer | No | Optional filters for location, customer, and creator. |
start_date, end_date | string | No | Optional transaction date range in Y-m-d. Applied only when both are present. |
kind | string | No | standard, quotation, proforma, or all. Defaults to all. |
q | string | No | Minimum 2 characters when sent. Matches ref_no, invoice_no, numeric id, and customer-facing contact fields. |
format | string | No | json (default) or csv. |
SellDraftListRow object| Field | Type | Description |
|---|---|---|
All SellListRow fields | mixed | The same base row shape returned by List sells. |
sub_status | string | null | Draft sub-type such as quotation, proforma, or null for standard drafts. |
is_quotation | boolean | Whether the row is marked internally as a quotation draft. |
| Field | Type | Description |
|---|---|---|
data | array<SellDraftListRow> | Paginated draft rows. |
meta.current_page, meta.last_page, meta.per_page, meta.total | integer | Pagination metadata. |
| Status | When it happens | Response shape |
|---|---|---|
200 | Draft rows were returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks draft or quotation list permissions, or the Add Sale module is disabled. | { "message": string } |
422 | The query string failed validation, such as a 1-character q. | Laravel validation JSON or { "message": string }. |
Returns one draft sale with the same detailed line and payment structure as finalized sell detail, plus draft-specific flags.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/sell-drafts/{id} |
| Permission | Same permission gate and draft visibility rules as List sell drafts. |
| CSV behavior | format=csv streams one row with a data_json column containing the full JSON payload. |
| Success response | 200 with a SellDraftDetail object. |
SellDraftDetail object| Field | Type | Description |
|---|---|---|
All SellDetail fields | mixed | The same detailed payload used by Get sell. |
is_quotation | boolean | Quotation flag loaded from the draft transaction. |
sub_status | string | null | Draft subtype such as quotation or proforma. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The draft was returned successfully. | { "data": SellDraftDetail } or CSV download. |
403 | The token user lacks list visibility for the draft row or the Add Sale module is disabled. | { "message": string } |
404 | The draft id is not visible in the draft query. | { "message": "Not found" } |
Updates a visible draft or quotation draft while keeping it in draft state.
| Property | Value |
|---|---|
| Method | PUT or PATCH |
| Path | /api/v1/integration/sell-drafts/{id} |
| Permission | quotation.update when the stored sub_status is quotation; otherwise draft.update. |
| Module requirement | The business must have the add_sale module enabled. |
| Subscription rule | Returns 402 when the business is not subscribed. |
| Edit window | transaction_edit_days is enforced. Existing sell returns also block updates. |
| Request encoding | application/json |
| Success response | 200 with the updated draft id, invoice number, and status. |
Create sale| Field | Type | Required | Description |
|---|---|---|---|
| All fields from Create sale | mixed | Mostly Yes | The endpoint reuses the same core payload shape as direct sale creation. |
status | string | Yes | Must stay draft; any other value returns 422. |
products[].transaction_sell_lines_id | integer | null | No | Optional existing sell-line id when updating previously saved draft rows. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The draft was updated successfully. | { "message": string, "data": { "id": integer, "invoice_no": string | null, "status": "draft" } } |
402 | The business is not subscribed. | { "message": string } |
403 | Demo mode is active, the Add Sale module is disabled, the token user lacks the correct update permission, or the chosen location is not permitted. | { "message": string } |
404 | The draft id is not visible in the draft query. | { "message": "Not found" } |
422 | The edit window expired, a sell return exists, the payload is invalid, the status is not draft, or invoice calculation/business rules rejected the update. | Laravel validation JSON or { "message": string }. |
500 | The draft update transaction failed unexpectedly. | { "message": string } |
Deletes a visible draft row, including quotation drafts, through the draft branch of the normal sale delete pipeline.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/v1/integration/sell-drafts/{id} |
| Permission | quotation.delete when the stored sub_status is quotation; otherwise draft.delete. |
| Module requirement | The business must have the add_sale module enabled. |
| Subscription rule | Returns 402 when the business is not subscribed. |
| Edit window | transaction_edit_days is enforced. Existing sell returns also block deletion. |
| Delete behavior | Delegates to TransactionUtil::deleteSale, so the same downstream draft-delete rules apply as in the app. |
| Success response | 200 with the deleted draft id. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The draft was deleted successfully. | { "message": string, "data": { "id": integer } } |
402 | The business is not subscribed. | { "message": string } |
403 | Demo mode is active, the Add Sale module is disabled, or the token user lacks the correct delete permission. | { "message": string } |
404 | The draft id is not visible in the draft query. | { "message": "Not found" } |
422 | The edit window expired, a sell return exists, or the delete pipeline rejected the draft. | { "message": string } |
500 | The draft delete transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Lists quotation drafts only.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/quotations |
| Permission | quotation.view_all or quotation.view_own. |
| Module requirement | The business must have the add_sale module enabled. |
| Row scope | Only draft rows where sub_status = quotation. |
| CSV behavior | format=csv streams all matching rows and ignores pagination. |
| Success response | 200 with paginated SellDraftListRow rows. |
| Parameter | Type | Required | Description |
|---|---|---|---|
All draft-list filters except kind | mixed | No | Uses the same filters as List sell drafts, but quotations are already isolated by the route. |
format | string | No | json (default) or csv. |
| Status | When it happens | Response shape |
|---|---|---|
200 | Quotation rows were returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks quotation list permission or the Add Sale module is disabled. | { "message": string } |
422 | The query string failed validation, such as a 1-character q. | Laravel validation JSON or { "message": string }. |
Returns one quotation draft with the same detailed payload shape as draft detail.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/quotations/{id} |
| Permission | Same permission gate and quotation visibility rules as List quotations. |
| CSV behavior | format=csv streams one row with a data_json column containing the full JSON payload. |
| Success response | 200 with a SellDraftDetail object. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The quotation was returned successfully. | { "data": SellDraftDetail } or CSV download. |
403 | The token user lacks quotation visibility or the Add Sale module is disabled. | { "message": string } |
404 | The quotation id is not visible in the quotation query. | { "message": "Not found" } |
Creates a quotation draft. The controller forces status = draft and draft_kind = quotation server-side, so the client can reuse the same payload shape as direct sale creation.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/quotations |
| Permission | Same as Create sale: sell.create or direct_sell.access. |
| Module, subscription, quota | The Add Sale module, subscription check, and invoice quota rules are all enforced the same way as direct sale creation. |
| Request body | Reuse the Create sale JSON body. Draft status and quotation kind are injected server-side. |
| Success response | 201 with the same response shape as Create sale. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The quotation draft was created successfully. | { "message": string, "data": { ... } } |
402 | The business is not subscribed or has no remaining invoice quota. | { "message": string } |
403 | Demo mode is active, the Add Sale module is disabled, or the token user lacks create permission. | { "message": string } |
422 | The reused create-sale payload failed validation or business-rule checks. | Laravel validation JSON or { "message": string }. |
500 | The quotation create transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Converts a visible quotation draft into a finalized invoice.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/quotations/{id}/convert-to-invoice |
| Permission | sell.create or direct_sell.access. |
| Module, subscription, quota | The Add Sale module must be enabled, the business must be subscribed, and invoice quota must still be available. |
| Demo mode | Returns 403 in demo environments. |
| Workflow | The controller assigns a new final invoice number, clears quotation flags, decreases stock, updates payment status, maps purchases, runs notifications, writes activity, and dispatches the sell modified event. |
| Special rules | Customer credit limits are enforced. POS/screen quotations (is_direct_sale = 0) require an open cash register before conversion. |
| Success response | 200 with finalized invoice metadata. |
| Field | Type | Description |
|---|---|---|
message | string | Localized conversion success message. |
data.id | integer | Converted sale id. |
data.invoice_no | string | null | New finalized invoice number. |
data.status | string | Always final after success. |
data.payment_status | string | null | Updated payment status after conversion. |
data.is_direct_sale | boolean | Preserved direct/POS mode flag from the quotation. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The quotation was converted successfully. | { "message": string, "data": { ... } } |
402 | The business is not subscribed or has no remaining invoice quota. | { "message": string } |
403 | Demo mode is active, the Add Sale module is disabled, or the token user lacks convert permission. | { "message": string } |
404 | The quotation id is not visible in the quotation query. | { "message": "Not found" } |
422 | The customer credit limit is exceeded, the cash register requirement is not met for a POS quotation, or stock/payment business rules reject the conversion. | { "message": string } |
500 | The quotation conversion transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Lists finalized sell-return transactions visible to the authenticated user.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/sell-returns |
| Permission | access_sell_return or access_own_sell_return. |
| Visibility rules | Only type = sell_return, status = final rows are returned. Permitted-location scope applies. Own-only users are limited to rows they created. |
| CSV behavior | format=csv streams all matching rows and ignores pagination. |
| Success response | 200 with paginated SellReturnListRow 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, created_by | integer | No | Optional location, customer, and creator filters. |
start_date, end_date | string | No | Optional transaction date range in Y-m-d. Applied only when both are present. |
q | string | No | Minimum 2 characters when sent. Matches the return invoice/ref, numeric id, customer-facing contact fields, and the parent sale invoice/ref. |
format | string | No | json (default) or csv. |
SellReturnListRow object| Field | Type | Description |
|---|---|---|
id | integer | Sell-return transaction id. |
invoice_no, ref_no | string | null | Return identifiers. |
transaction_date | string | null | ISO-8601 return timestamp. |
status, payment_status | string | null | Return status and payment status. |
final_total, total_before_tax, tax_amount, total_paid | number | null | Return totals. |
return_parent_id | integer | null | Parent finalized sale id. |
parent_sale | object | null | Parent sale summary with id, invoice_no, and ref_no. |
contact | object | null | Contact summary with id, name, mobile, contact_id, and supplier_business_name. |
location | object | null | Location summary with id and name. |
| Status | When it happens | Response shape |
|---|---|---|
200 | Sell returns were returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks sell-return list permission. | { "message": "Unauthorized" } |
422 | The query string failed validation, such as a 1-character q. | Laravel validation JSON or { "message": string }. |
Returns one sell-return transaction with parent-sale linkage, returned lines, and payment summaries.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/sell-returns/{id} |
| Permission | Same permission gate and visibility rules as List sell returns. |
| CSV behavior | format=csv streams one row with a data_json column containing the full JSON payload. |
| Success response | 200 with a SellReturnDetail object. |
SellReturnLine object| Field | Type | Description |
|---|---|---|
sell_line_id, product_id, variation_id | integer | Identifiers for the original parent sale line. |
product_label | string | Formatted parent sale product label. |
sub_sku | string | null | Variation SKU. |
quantity_returned | number | Returned quantity for that line. |
unit_price_inc_tax | number | null | Stored unit price including tax from the parent sale line. |
line_total | number | Returned quantity multiplied by unit_price_inc_tax. |
SellReturnDetail object| Field | Type | Description |
|---|---|---|
id, invoice_no, ref_no | integer | string | null | Core return identifiers. |
transaction_date | string | null | ISO-8601 return timestamp. |
status, payment_status | string | null | Return status and payment status. |
final_total, total_before_tax, tax_amount, total_paid | number | null | Return totals. |
return_parent_id | integer | null | Parent finalized sale id. |
parent_sale | object | null | Parent sale summary with id, invoice_no, ref_no, and transaction_date. |
contact | object | null | Contact summary with id, name, mobile, email, contact_id, and supplier_business_name. |
location | object | null | Location summary with id and name. |
order_tax | object | null | Tax summary with id, name, amount, and is_tax_group. |
lines | array<SellReturnLine> | Returned parent-sale lines where quantity_returned > 0. |
payments | array<SellPaymentSummary> | Return payment summaries using the same shape as sell detail payments. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The sell return was returned successfully. | { "data": SellReturnDetail } or CSV download. |
403 | The token user lacks sell-return visibility. | { "message": "Unauthorized" } |
404 | The sell return id is not visible in the sell-return query. | { "message": "Not found" } |
Creates or updates a sell return against a finalized sale using the same return pipeline as the web app.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/sell-returns |
| Permission | access_sell_return or access_own_sell_return. |
| Subscription rule | Returns 402 when the business is not subscribed. |
| Parent sale rules | The parent sale must be a finalized sell in the current business, within permitted locations, and visible to the current user under the sell-return access rules. |
| Success response | 201 with the created sell-return id, invoice number, and parent sale id. |
| Field | Type | Required | Description |
|---|---|---|---|
transaction_id | integer | Yes | Parent finalized sale id. |
returns | array | Yes | At least one returned line using the SellReturnRequestLine shape below. |
discount_type, discount_amount | string | number | null | No | Optional return-level discount. Defaults from the parent sale when omitted. |
tax_id | integer | null | No | Optional tax id from the same business. Defaults from the parent sale when omitted. |
invoice_no | string | null | No | Optional custom return invoice number. |
transaction_date | string | null | No | Optional return date/datetime. |
SellReturnRequestLine object| Field | Type | Required | Description |
|---|---|---|---|
sell_line_id | integer | Yes | Must belong to the parent sale identified by transaction_id. |
quantity | number | Yes | Requested return quantity in the same unit context as the sale line. The controller converts sub-units before enforcing the sold-quantity ceiling. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The sell return was created successfully. | { "message": string, "data": { "id": integer, "invoice_no": string | null, "parent_sale_id": integer } } |
402 | The business is not subscribed. | { "message": string } |
403 | Demo mode is active or the token user lacks sell-return access. | { "message": string } |
404 | The parent sale is outside the user's sell-return visibility scope. | { "message": "Not found" } |
422 | A return line does not belong to the parent sale, all requested quantities are zero on a new return, a quantity exceeds the sold quantity, validation fails, or the core return pipeline rejects the request. | Laravel validation JSON or { "message": string }. |
500 | The sell-return create transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Records a refund payment against a sell-return transaction.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/sell-returns/{id}/payments |
| Permission | sell.payments plus sell-return visibility through the finalized sell-return query. |
| Subscription rule | Returns 402 when the business is not subscribed. |
| Success response | 201 with the created payment summary. |
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Refund amount. Must be at least 0.01. |
method | string | Yes | Payment method key enabled for the return location. |
paid_on, note | string | null | No | Optional payment datetime and note. |
account_id | integer | null | No | Optional account id. It is ignored for advance payments. |
card_number, card_holder_name, card_transaction_number, card_type, card_month, card_year, card_security | string | null | No | Card-payment metadata. |
cheque_number, bank_account_number | string | null | No | Cheque or bank reference data. |
transaction_no_1 to transaction_no_3 | string | null | No | Reference values for custom payment methods. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The refund payment was recorded successfully. | { "message": string, "data": { ... } } |
402 | The business is not subscribed. | { "message": string } |
403 | Demo mode is active, the token user lacks sell.payments, or the return is not visible. | { "message": string } |
404 | The sell return id is not visible in the sell-return query. | { "message": "Not found" } |
422 | The return is already fully paid, the payment method is invalid for the location, validation fails, or an advance payment exceeds the contact's available balance. | Laravel validation JSON or { "message": string }. |
500 | The refund payment transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Deletes a sell return and reverses the stored return quantities and stock effects.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/v1/integration/sell-returns/{id} |
| Permission | Same permission gate and visibility rules as List sell returns. |
| Subscription rule | Returns 402 when the business is not subscribed. |
| Delete behavior | The controller clears quantity_returned on parent sale lines, updates sold quantities, restores stock at the return location, deletes the return row, and dispatches TransactionPaymentDeleted for loaded payment rows. |
| Success response | 200 with the deleted sell-return id. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The sell return was deleted successfully. | { "message": string, "data": { "id": integer } } |
402 | The business is not subscribed. | { "message": string } |
403 | Demo mode is active or the token user lacks sell-return access. | { "message": string } |
404 | The sell return id is not visible in the sell-return query. | { "message": "Not found" } |
422 | The core reversal pipeline rejects the delete, such as on purchase/sell mismatch conditions. | { "message": string } |
500 | The delete transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Response 200 — message and data.id. 404 if not found or not visible. 422 on purchase/sell mapping errors.
Lists finalized sales that are currently in the shipping workflow.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/shipments |
| Permission | Admin access or any of access_shipping, access_own_shipping, or access_commission_agent_shipping. |
| Row scope | Only finalized sells with a non-null shipping_status. project_invoice rows are excluded. |
| Visibility rules | Permitted-location scope applies. Own and commission-agent shipment permissions use the same scoping as the web shipment screen. Users with access_pending_shipments_only never receive delivered rows. |
| CSV behavior | format=csv streams all matching rows and ignores pagination. |
| Success response | 200 with paginated ShipmentRow 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, created_by | integer | No | Optional location, customer, and creator filters. |
start_date, end_date | string | No | Optional transaction date range in Y-m-d. Applied only when both are present. |
shipping_status | string | No | One of ordered, packed, shipped, delivered, or cancelled. |
delivery_person | integer | No | Optional delivery-person user id filter. |
only_pending | boolean-like string | No | When true, excludes delivered rows even if the user normally has access to them. |
q | string | No | Minimum 2 characters when sent. Matches the sale invoice/ref, numeric id, and linked customer-facing contact fields. |
format | string | No | json (default) or csv. |
ShipmentRow object| Field | Type | Description |
|---|---|---|
id | integer | Underlying finalized sale id. |
invoice_no, ref_no | string | null | Sale identifiers. |
transaction_date | string | null | ISO-8601 sale timestamp. |
status, payment_status | string | null | Sale status and payment status. |
final_total, total_before_tax, tax_amount, discount_amount, total_paid | number | null | Sale totals. |
is_direct_sale | boolean | Whether the shipment comes from a direct sale. |
sub_type | string | null | Sale sub-type when present. |
shipping_status | string | null | Current shipment workflow status. |
shipping_details, shipping_address, delivered_to | string | null | Shipping text fields from the sale. |
delivery_person | integer | null | Assigned delivery-person user id. |
shipping_custom_field_1 to shipping_custom_field_5 | string | null | Custom shipment fields. |
contact | object | null | Contact summary with id, name, mobile, contact_id, and supplier_business_name. |
location | object | null | Location summary with id and name. |
| Status | When it happens | Response shape |
|---|---|---|
200 | Shipment rows were returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks shipment access. | { "message": "Unauthorized" } |
422 | The query string failed validation, such as a 1-character q. | Laravel validation JSON or { "message": string }. |
Returns one shipment row by finalized sale id.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/shipments/{id} |
| Permission | Same permission gate and visibility rules as List shipments. |
| CSV behavior | format=csv streams one row with a data_json column containing the full JSON payload. |
| Success response | 200 with one ShipmentRow object. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The shipment row was returned successfully. | { "data": ShipmentRow } or CSV download. |
403 | The token user lacks shipment access. | { "message": "Unauthorized" } |
404 | The finalized sale is outside the shipment list visibility scope. | { "message": "Not found" } |
Returns the allowed shipping-status keys and labels used by the shipment list and update endpoints.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/shipments/shipping-statuses |
| Permission | Same shipment-access gate used by the shipment list. |
| CSV behavior | format=csv streams the same rows as JSON with key and label columns. |
| Success response | 200 with data[] status rows. |
ShippingStatusRow object| Field | Type | Description |
|---|---|---|
key | string | Status key used by the API, such as ordered or delivered. |
label | string | Translated display label. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The shipping statuses were returned successfully. | { "data": [ { "key": string, "label": string } ] } or CSV download. |
403 | The token user lacks shipment access. | { "message": "Unauthorized" } |
Updates shipment-related fields on a finalized sale. This endpoint uses shipment-context visibility, which means the sale does not have to already be in the shipment list as long as it is a visible finalized sale.
| Property | Value |
|---|---|
| Method | PUT or PATCH |
| Path | /api/v1/integration/shipments/{id} |
| Permission | Same shipment-access gate used by the shipment list. |
| Demo mode | Returns 403 in demo environments. |
| Request encoding | application/json |
| Activity note | shipping_note is written only into the activity log payload. It is not a transaction column. |
| Success response | 200 with the sale id, resulting shipping_status, and delivery_person. |
| Field | Type | Required | Description |
|---|---|---|---|
shipping_details, shipping_address | string | null | No | Optional shipping text fields. |
shipping_status | string | null | No | One of ordered, packed, shipped, delivered, or cancelled. |
delivered_to | string | null | No | Optional recipient text. |
delivery_person | integer | null | No | Optional delivery-person user id. |
shipping_custom_field_1 to shipping_custom_field_5 | string | null | No | Optional custom shipment fields. |
shipping_note | string | null | No | Optional activity-log note. Sending only this field is not enough; at least one persisted shipment field must also be present. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The shipment fields were updated successfully. | { "message": string, "data": { "id": integer, "shipping_status": string | null, "delivery_person": integer | null } } |
403 | Demo mode is active or the token user lacks shipment access. | { "message": string } |
404 | The finalized sale is outside the shipment update visibility scope. | { "message": "Not found" } |
422 | No persisted shipment fields were supplied or validation failed. | Laravel validation JSON or { "message": string }. |
500 | The shipment update failed unexpectedly. | { "message": "something_went_wrong" } |
Lists discounts configured for the current business.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/discounts |
| Permission | discount.access. |
| Scope | Business-scoped discounts from the Discounts screen, joined with brand, category, and location names. |
| CSV behavior | format=csv streams all matching rows and ignores pagination. The variations array is JSON-encoded in CSV cells. |
| Success response | 200 with paginated DiscountRow rows. |
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page, page | integer | No | Pagination controls. per_page accepts 1 to 100 and defaults to 20. |
is_active | boolean-like string | No | Optional active-state filter. Accepts 0, 1, true, or false. |
q | string | No | Minimum 2 characters when sent. Matches discount name or numeric id. |
format | string | No | json (default) or csv. |
VariationSummary object| Field | Type | Description |
|---|---|---|
id, product_id | integer | Variation and product ids. |
sub_sku | string | null | Variation SKU. |
label | string | Resolved product and variation label used by the discount API. |
DiscountRow object| Field | Type | Description |
|---|---|---|
id, business_id | integer | Discount identifiers. |
name | string | Discount name. |
brand_id, category_id, location_id | integer | null | Linked brand, category, and location ids. |
priority | integer | null | Discount priority. |
discount_type | string | null | fixed or percentage. |
discount_amount | number | null | Configured discount amount. |
starts_at, ends_at | string | null | ISO-8601 start and end timestamps. |
is_active, applicable_in_cg | boolean | Activation and customer-group flags. |
spg | string | null | Selling price group id stored as a string. |
brand_name, category_name, location_name | string | null | Joined display names. |
variations | array<VariationSummary> | Resolved variation list attached to the discount. |
| Status | When it happens | Response shape |
|---|---|---|
200 | Discount rows were returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks discount.access. | { "message": "Unauthorized" } |
422 | The query string failed validation, such as a 1-character q. | Laravel validation JSON or { "message": string }. |
Returns one discount row by id.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/discounts/{id} |
| Permission | discount.access. |
| CSV behavior | format=csv streams one row with a data_json column containing the full JSON payload. |
| Success response | 200 with one DiscountRow object. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The discount was returned successfully. | { "data": DiscountRow } or CSV download. |
403 | The token user lacks discount.access. | { "message": "Unauthorized" } |
404 | No discount with that id exists in the current business. | { "message": "Not found" } |
Creates a new discount for the current business.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/discounts |
| Permission | discount.access. |
| Demo mode | Returns 403 in demo environments. |
| Request encoding | application/json |
| Success response | 201 with the full created DiscountRow object. |
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Discount name. |
location_id | integer | Yes | Business location id. |
priority | integer | Yes | Priority value, minimum 0. |
discount_type | string | Yes | fixed or percentage. |
discount_amount | number | Yes | Discount amount, minimum 0. |
brand_id, category_id | integer | null | No | Optional brand and category ids from the current business. |
starts_at, ends_at | string | null | No | Optional date or datetime values. On create, ends_at must be on or after starts_at. |
is_active, applicable_in_cg | boolean | No | Optional booleans. Defaults are true and false respectively when omitted. |
spg | integer | string | null | No | Optional selling price group id from the current business. |
variation_ids | array<integer> | null | No | Optional attached variation ids. Every id must belong to a variation inside the current business. When this array is non-empty, the controller clears brand_id and category_id before saving. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The discount was created successfully. | { "message": string, "data": DiscountRow } |
403 | Demo mode is active or the token user lacks discount.access. | { "message": string } |
422 | Validation failed or one or more variation_ids do not belong to this business. Invalid variation ids are returned in invalid_variation_ids. | Laravel validation JSON or { "message": string, "invalid_variation_ids": [...] }. |
500 | The discount create transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Partially updates an existing discount.
| Property | Value |
|---|---|
| Method | PUT or PATCH |
| Path | /api/v1/integration/discounts/{id} |
| Permission | discount.access. |
| Demo mode | Returns 403 in demo environments. |
| Request style | Partial update. Omit keys you want to keep unchanged. |
| Success response | 200 with the full updated DiscountRow object. |
| Field | Type | Description |
|---|---|---|
| Any field from Add discount | mixed | All create fields are reusable on update as optional fields. |
variation_ids | array<integer> | null | When the key is omitted, attached variations stay unchanged. When the key is sent, including [], the relation is synced to that exact array. A non-empty array clears brand_id and category_id before saving. |
ends_at | string | null | On update, the controller compares it against the incoming or existing starts_at and rejects an earlier end date. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The discount was updated successfully. | { "message": string, "data": DiscountRow } |
403 | Demo mode is active or the token user lacks discount.access. | { "message": string } |
404 | No discount with that id exists in the current business. | { "message": "Not found" } |
422 | Validation failed, the end date is earlier than the effective start date, or one or more variation_ids do not belong to this business. | Laravel validation JSON or { "message": string, "invalid_variation_ids": [...] }. |
500 | The discount update transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Deletes a discount by id.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/v1/integration/discounts/{id} |
| Permission | discount.access. |
| Demo mode | Returns 403 in demo environments. |
| Success response | 200 with the deleted discount id. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The discount was deleted successfully. | { "message": string, "data": { "id": integer } } |
403 | Demo mode is active or the token user lacks discount.access. | { "message": string } |
404 | No discount with that id exists in the current business. | { "message": "Not found" } |
500 | The discount delete transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Reactivates a discount by forcing is_active back to true.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/discounts/{id}/activate |
| Permission | discount.access. |
| Demo mode | Returns 403 in demo environments. |
| Success response | 200 with the activated discount id and is_active = true. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The discount was activated successfully. | { "message": string, "data": { "id": integer, "is_active": true } } |
403 | Demo mode is active or the token user lacks discount.access. | { "message": string } |
404 | No discount with that id exists in the current business. | { "message": "Not found" } |
Lists finalized POS sales only.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/pos/sells |
| Permission | Same permission gate as List sells. |
| Module requirement | The business must have the pos_sale module enabled. |
| Row scope | Uses the finalized sell list query but forces is_direct_sale = 0 and sub_type = null. |
| Query and response | Uses the same filters, pagination, CSV behavior, and SellListRow response shape as List sells. |
| Status | When it happens | Response shape |
|---|---|---|
200 | POS sales were returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks sell-list access or the pos_sale module is disabled. | { "message": string } |
422 | The query string failed validation. | Laravel validation JSON or { "message": string }. |
Returns recent POS transactions created by the authenticated user.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/pos/recent-transactions |
| Permission | Same permission gate as List POS sales. |
| Module requirement | The business must have the pos_sale module enabled. |
| Row scope | Only rows created by the current user, with type = sell and is_direct_sale = 0. |
| Success response | 200 with PosRecentRow rows and lightweight meta fields. |
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Defaults to final. Use draft for non-quotation drafts, quotation for quotation drafts, or any other stored transaction status value. |
transaction_sub_type | string | null | No | When omitted, only rows with sub_type = null are returned. When present, filters to that exact sub_type. |
limit | integer | No | Maximum rows to return, from 1 to 50. Defaults to the configured POS recent-transactions limit, capped at 50. |
format | string | No | json (default) or csv. |
PosRecentRow object| Field | Type | Description |
|---|---|---|
All SellListRow fields | mixed | The same base fields returned by the finalized sell list. |
sub_status | string | null | Draft subtype, such as quotation, when applicable. |
is_suspend | boolean | Whether the POS sale is held. |
table | object | null | Restaurant table summary with id and name when set. |
created_at | string | null | ISO-8601 creation timestamp. |
| Field | Type | Description |
|---|---|---|
data | array<PosRecentRow> | Recent POS rows, capped by limit. |
meta.status, meta.transaction_sub_type | string | null | Echoes the effective filters. |
meta.limit, meta.count | integer | Requested limit and returned row count. |
| Status | When it happens | Response shape |
|---|---|---|
200 | Recent POS transactions were returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks sell-list access or the pos_sale module is disabled. | { "message": string } |
422 | The query string failed validation. | Laravel validation JSON. |
Creates a POS sale or held POS sale. The request body reuses the direct-sale create payload, but the controller enforces POS-specific register behavior.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/pos/sells |
| Permission | sell.create, direct_sell.access, or so.create. |
| Module requirement | The business must have the pos_sale module enabled. |
| Subscription and quota | The business must be subscribed and still have invoice quota available. |
| Register rule | An open cash register is required before creating POS sales. Held final POS sales skip immediate payment posting but still require the module and normal create permissions. |
| Request body | Reuse the Create sale JSON body, plus optional is_suspend. |
| POS behavior | The controller forces is_direct_sale = 0. Final non-suspended non-credit sales also post payment rows to the open cash register. |
| Success response | 201 with the same response shape as Create sale, but is_direct_sale is always false. |
| Field | Type | Required | Description |
|---|---|---|---|
is_suspend | boolean | No | When true on a final POS sale, the sale is created as held. Payment lines and cash-register rows are deferred until finalization, but stock movement still follows the normal final-sale flow. |
consumption_type | string | null | No | Same optional field as Create sale (sales, damages, or sampling). |
| Status | When it happens | Response shape |
|---|---|---|
201 | The POS sale was created successfully. | { "message": string, "data": { ... } } |
402 | The business is not subscribed or has reached invoice quota. | { "message": string } |
403 | Demo mode is active, the token user lacks POS create permission, the pos_sale module is disabled, or the chosen location is not permitted. | { "message": string } |
422 | The cash register requirement is not met, the create-sale payload is invalid, a payment method is invalid for the location, or other stock/payment business rules reject the POS sale. | Laravel validation JSON or { "message": string }. |
500 | The POS create transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Lists held POS invoices only.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/pos/suspended-sales |
| Permission | Same permission gate as List sells. |
| Module requirement | The business must have the pos_sale module enabled. |
| Row scope | Only finalized rows where is_direct_sale = 0, sub_type = null, and is_suspend = 1. |
| Success response | 200 with paginated SuspendedPosRow rows. |
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page, page | integer | No | Pagination controls. per_page accepts 1 to 100 and defaults to 20. |
location_id | integer | No | Optional held-sale location filter. |
q | string | No | Minimum 2 characters when sent. Matches invoice/ref, numeric id, and customer-facing contact fields. |
format | string | No | json (default) or csv. |
SuspendedPosRow object| Field | Type | Description |
|---|---|---|
All SellListRow fields | mixed | The same base sell-list shape. |
is_suspend | boolean | Always true for this endpoint. |
| Status | When it happens | Response shape |
|---|---|---|
200 | Held POS rows were returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks sell-list access or the pos_sale module is disabled. | { "message": string } |
422 | The query string failed validation. | Laravel validation JSON or { "message": string }. |
Returns one held POS sale with full sell detail and is_suspend = true.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/pos/suspended-sales/{id} |
| Permission | Same permission gate and visibility rules as List suspended POS sales. |
| CSV behavior | format=csv streams one row with a data_json column containing the full JSON payload. |
| Success response | 200 with the SellDetail payload plus is_suspend = true. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The held POS sale was returned successfully. | { "data": SellDetail & { is_suspend: true } } or CSV download. |
403 | The token user lacks sell-list access or the pos_sale module is disabled. | { "message": string } |
404 | The held POS sale id is not visible in the suspended-sale query. | { "message": "Not found" } |
Finalizes a held POS sale by clearing the suspended flag and recording checkout payments or credit-sale state.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/pos/suspended-sales/{id}/finalize |
| Permission | At least one of sell.update, direct_sell.access, so.update, or edit_pos_payment. |
| Module and subscription | The business must have the pos_sale module enabled and an active subscription. |
| Demo mode | Returns 403 in demo environments. |
| Target row | The sale must be visible, held, POS-only, and have no existing payment lines. |
| Register rule | An open cash register is required unless is_credit_sale is true. |
| Success response | 200 with finalized invoice metadata. |
| Field | Type | Required | Description |
|---|---|---|---|
is_credit_sale | boolean | No | When true, the suspended sale is finalized without payment rows. |
payments | array | null | Conditional | Required when is_credit_sale is false. Uses the same payment row shape as FinalPaymentLine. |
change_return | number | null | No | Optional cash change row appended as a cash is_return payment line. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The held POS sale was finalized successfully. | { "message": string, "data": { ... } } |
402 | The business is not subscribed. | { "message": string } |
403 | Demo mode is active, the token user lacks finalize permission, or the pos_sale module is disabled. | { "message": string } |
404 | The held POS sale id is not visible in the finalize query. | { "message": "Not found" } |
422 | The held sale already has payment lines, the request omitted required payments for non-credit checkout, the payment method is invalid for the location, the cash register requirement is not met, or an advance payment exceeds the contact balance. | Laravel validation JSON or { "message": string }. |
500 | The finalize transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Deletes a held POS sale that has not yet collected any payment lines.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/v1/integration/pos/suspended-sales/{id} |
| Permission | At least one of sell.delete, direct_sell.delete, or so.delete. |
| Module and subscription | The business must have the pos_sale module enabled and an active subscription. |
| Demo mode | Returns 403 in demo environments. |
| Delete rules | The row must still be a visible held POS sale and must have zero payment-line balance. The delete then delegates to TransactionUtil::deleteSale. |
| Success response | 200 with the deleted held-sale id. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The held POS sale was deleted successfully. | { "message": string, "data": { "id": integer } } |
402 | The business is not subscribed. | { "message": string } |
403 | Demo mode is active, the token user lacks delete permission, or the pos_sale module is disabled. | { "message": string } |
404 | The held POS sale id is not visible in the delete query. | { "message": "Not found" } |
422 | The held sale already has payment lines or the delete pipeline rejects the sale because of business rules such as returns or compliance blockers. | { "message": string } |
500 | The delete transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Lists open and closed cash-register sessions visible to the token user.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/cash-registers |
| Permission | view_cash_register. |
| Module requirement | The business must have the pos_sale module enabled. |
| Visibility | Rows are limited to the token user's permitted locations. When the user is location-restricted, sessions with location_id = null still remain visible. |
| CSV behavior | format=csv streams all matching rows with a UTF-8 BOM and ignores page and per_page. |
| Success response | 200 with paginated CashRegisterRow rows. |
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page, page | integer | No | Pagination controls. per_page accepts 1 to 100 and defaults to 20. |
location_id | integer | No | Filters by the register session location id. |
user_id | integer | No | Filters sessions opened by a specific user id. |
status | string | No | Restricts rows to open or close. |
start_date, end_date | string | Conditional | Optional register-opened date range. Both values must be sent together, are parsed with the business date format, and start_date must not be after end_date. |
format | string | No | json (default) or csv. |
CashRegisterRow object| Field | Type | Description |
|---|---|---|
id | integer | Cash register session id. |
user_id, location_id | integer | null | User and business-location ids linked to the session. |
user_name, user_email, location_name | string | null | Joined display fields for the session owner and location. |
status | string | open or close. |
opened_at, closed_at | string | null | ISO-8601 timestamps for when the session opened and closed. |
closing_amount | number | null | Recorded closing cash amount after the register is closed. |
total_card_slips, total_cheques | integer | null | Counts recorded during closing. |
| Field | Type | Description |
|---|---|---|
data | array<CashRegisterRow> | 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 register list was returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks view_cash_register or the pos_sale module is disabled. | { "message": string } |
422 | The query string failed validation, only one date boundary was sent, or start_date is after end_date. | Laravel validation JSON or { "message": string }. |
Returns one visible cash-register session by id.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/cash-registers/{id} |
| Permission | Same permission and visibility rules as List cash registers. |
| CSV behavior | format=csv streams one UTF-8 BOM row whose data_json column matches the JSON data payload. |
| Success response | 200 with one CashRegisterRow object. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The cash register session was returned successfully. | { "data": CashRegisterRow } or CSV download. |
403 | The token user lacks view_cash_register or the pos_sale module is disabled. | { "message": string } |
404 | The register id does not exist in the visible session query. | { "message": "Not found" } |
422 | The query string failed validation. | Laravel validation JSON. |
Returns the visible register row, aggregate tender totals, and POS sales breakdown used by the register-details modal.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/cash-registers/{id}/details |
| Permission | Same permission and visibility rules as Get cash register. |
| Calculation basis | Uses CashRegisterUtil::getRegisterDetails() and CashRegisterUtil::getRegisterTransactionDetails(). The POS breakdown only includes finalized POS sells (type=sell, status=final, is_direct_sale=0) created by the register user between open and close time. |
| Types of service breakdown | types_of_service_details is only populated when the types_of_service module is enabled for the business. |
| CSV behavior | format=csv streams one UTF-8 BOM row whose data_json column matches the JSON data payload. |
| Success response | 200 with register detail objects. |
CashRegisterDetailsSummary object| Field | Type | Description |
|---|---|---|
user_id, location_id | integer | null | User and location ids for the register session. |
user_name, email, location_name, closing_note | string | null | Joined display fields and the saved close note. |
open_time, closed_at | string | null | ISO-8601 timestamps for the session boundaries. |
denominations | array | object | string | null | Decoded denominations JSON when available, otherwise the raw stored value. |
cash_in_hand | number | null | Opening float recorded through the initial cash-register transaction. |
total_sale, total_expense, total_refund | number | null | Aggregated sell, expense, and refund movement totals for the register. |
total_cash, total_cheque, total_card, total_bank_transfer, total_other, total_advance, total_custom_pay_1 ... total_custom_pay_7 | number | null | Sales totals grouped by payment method. |
total_cash_expense, total_cheque_expense, total_card_expense, total_bank_transfer_expense, total_other_expense, total_advance_expense, total_custom_pay_1_expense ... total_custom_pay_7_expense | number | null | Expense totals grouped by payment method. |
total_cash_refund, total_cheque_refund, total_card_refund, total_bank_transfer_refund, total_other_refund, total_advance_refund, total_custom_pay_1_refund ... total_custom_pay_7_refund | number | null | Refund totals grouped by payment method. |
total_cheques, total_card_slips | integer | null | Counts of cheque and card-slip entries recorded in the session. |
CashRegisterTransactionBreakdown object| Field | Type | Description |
|---|---|---|
product_details_by_brand | array<object> | Each row contains brand_name, total_quantity, and total_amount for finalized POS sales grouped by brand. |
product_details | array<object> | Each row contains product_name, product_type, variation_name, product_variation_name, sku, total_quantity, and total_amount. |
types_of_service_details | array<object> | null | When enabled, each row contains types_of_service_name and total_sales. |
transaction_details | object | null | Contains aggregated finalized POS totals: total_tax, total_discount, total_sales, and total_shipping_charges. |
| Field | Type | Description |
|---|---|---|
data.register | CashRegisterRow | The same row shape returned by Get cash register. |
data.summary | CashRegisterDetailsSummary | Aggregate tender and refund totals for the register. |
data.transaction_breakdown | CashRegisterTransactionBreakdown | POS sales breakdown grouped by brand, product variation, and optionally type of service. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The cash register details were returned successfully. | { "data": { "register": { ... }, "summary": { ... }, "transaction_breakdown": { ... } } } or CSV download. |
403 | The token user lacks view_cash_register or the pos_sale module is disabled. | { "message": string } |
404 | The register id is not visible or the register summary could not be generated. | { "message": "Not found" } |
422 | The query string failed validation. | Laravel validation JSON. |
Lists the raw cash-register transaction lines for one visible session.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/cash-registers/{id}/transactions |
| Permission | Same permission and visibility rules as Get cash register. |
| Included rows | Ordered cash_register_transactions lines such as opening float, sell payments, transfers, expenses, and refunds. |
| CSV behavior | format=csv streams all matching lines with a UTF-8 BOM and ignores page and per_page. |
| Success response | 200 with paginated CashRegisterTransactionRow rows. |
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page, page | integer | No | Pagination controls. per_page accepts 1 to 100 and defaults to 20. |
format | string | No | json (default) or csv. |
CashRegisterTransactionRow object| Field | Type | Description |
|---|---|---|
id, cash_register_id | integer | Transaction-line id and owning register session id. |
amount | number | null | Signed amount stored on the register line. |
pay_method | string | null | Payment method such as cash, card, cheque, bank_transfer, or custom payment keys. |
type | string | null | credit or debit. |
transaction_type | string | null | Register line source, such as initial, sell, transfer, expense, or refund. |
transaction_id | integer | null | Optional linked transaction id when the line came from a sale or another business transaction. |
created_at, updated_at | string | null | ISO-8601 timestamps for the register line. |
| Field | Type | Description |
|---|---|---|
data | array<CashRegisterTransactionRow> | The current result page. |
meta.cash_register_id | integer | Echoes the requested register id. |
meta.current_page, meta.last_page, meta.per_page, meta.total | integer | Laravel paginator metadata. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The register transaction lines were returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks view_cash_register or the pos_sale module is disabled. | { "message": string } |
404 | The register id is not visible in the register-session query. | { "message": "Not found" } |
422 | The query string failed validation. | Laravel validation JSON. |
Opens a new cash-register session for the authenticated user.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/cash-registers/open |
| Permission | view_cash_register. |
| Module requirement | The business must have the pos_sale module enabled. |
| Demo mode | Returns 403 in demo environments. |
| Open-session rule | The authenticated user must not already have an open cash register. |
| Location rule | location_id must belong to the business and be inside the token user's permitted locations when location restrictions apply. |
| Success response | 201 with the newly opened register id and location metadata. |
| Field | Type | Required | Description |
|---|---|---|---|
location_id | integer | Yes | Business location id for the register session. |
initial_amount | number | null | No | Optional opening float. When greater than zero, the controller creates an initial cash credit line. |
| Field | Type | Description |
|---|---|---|
message | string | Localized success message. |
data.id, data.user_id, data.location_id | integer | Identifiers for the newly opened session. |
data.location_name | string | null | Resolved business-location name. |
data.status | string | Always open. |
data.opened_at | string | null | ISO-8601 timestamp for the session start. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The cash register session was opened successfully. | { "message": string, "data": { ... } } |
403 | Demo mode is active, the token user lacks view_cash_register, the pos_sale module is disabled, or the requested location is not allowed for the user. | { "message": string } |
422 | The user already has an open cash register or the request body failed validation. | Laravel validation JSON or { "message": string }. |
500 | The open-register transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Closes an open cash-register session for the authenticated user or for a specified business user.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/cash-registers/close |
| Permission | close_cash_register. |
| Module requirement | The business must have the pos_sale module enabled. |
| Demo mode | Returns 403 in demo environments. |
| Target register rule | If user_id is omitted, the controller closes the authenticated user's open register. When provided, it looks for an open register belonging to that business user. |
| Success response | 200 with the closed register summary row. |
| Field | Type | Required | Description |
|---|---|---|---|
user_id | integer | null | No | Optional business user id whose open register should be closed. |
closing_amount | number | Yes | Closing cash amount saved on the register. |
total_card_slips, total_cheques | integer | null | No | Optional non-negative counts recorded during closing. |
closing_note | string | null | No | Optional note up to 1000 characters. |
denominations | array | null | No | Optional denominations payload stored as JSON. |
| Field | Type | Description |
|---|---|---|
message | string | Localized close-success message. |
data.id, data.user_id, data.location_id | integer | null | Identifiers for the closed register session. |
data.status | string | After success this is close. |
data.opened_at, data.closed_at | string | null | ISO-8601 timestamps for the register session. |
data.closing_amount | number | null | Recorded closing amount. |
data.total_card_slips, data.total_cheques | integer | null | Saved closing counts. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The cash register session was closed successfully. | { "message": string, "data": { ... } } |
403 | Demo mode is active, the token user lacks close_cash_register, or the pos_sale module is disabled. | { "message": string } |
422 | No open cash register exists for the target user or the request body failed validation. | Laravel validation JSON or { "message": string }. |
500 | The close-register transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Lists sales-order transactions that are visible to the token user.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/sales-orders |
| Permission | At least one of so.view_all, so.view_own, or so.create. |
| Module requirement | The business must have POS settings -> Enable sales order enabled. |
| Visibility | Rows are always limited to the token user's permitted locations. If the user lacks so.view_all but has so.view_own, only rows created by that user are returned. |
| CSV behavior | format=csv streams all matching rows with a UTF-8 BOM and ignores page and per_page. |
| Success response | 200 with paginated SalesOrderRow rows. |
| Parameter | Type | Required | Description |
|---|---|---|---|
per_page, page | integer | No | Pagination controls. per_page accepts 1 to 100 and defaults to 20. |
location_id | integer | No | Filters rows by business-location id. |
contact_id | integer | No | Filters rows by customer contact id. |
status | string | No | Restricts rows to ordered, partial, or completed. |
shipping_status | string | No | Exact match against the stored sales-order shipping status. |
start_date, end_date | string | No | Optional Y-m-d date bounds applied to transaction_date only when both are present. end_date must be on or after start_date. |
q | string | No | Minimum 2 characters when sent. Matches invoice number, document, numeric id, and linked contact fields such as name, business name, mobile, and contact_id. |
format | string | No | json (default) or csv. |
SalesOrderRow object| Field | Type | Description |
|---|---|---|
id | integer | Sales-order transaction id. |
invoice_no, document | string | null | Order reference fields stored on the transaction. |
transaction_date | string | null | ISO-8601 transaction timestamp. |
status, shipping_status | string | null | Order workflow state and shipping state. |
final_total, total_before_tax, tax_amount, discount_amount | number | null | Order totals from the transaction header. |
so_qty_remaining | number | null | Remaining uninvoiced quantity across parent sell lines. |
contact | object | null | Customer 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<SalesOrderRow> | 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 sales-order list was returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks the required sales-order permission or sales orders are disabled in POS settings. | { "message": string } |
422 | The query string failed validation or q was shorter than 2 characters. | Laravel validation JSON or { "message": string }. |
Returns one visible sales order with parent sell lines and remaining uninvoiced quantities.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/sales-orders/{id} |
| Permission | Same permission and visibility rules as List sales orders. |
| CSV behavior | format=csv streams one UTF-8 BOM row whose data_json column matches the JSON data payload. |
| Success response | 200 with one SalesOrderDetail object. |
SalesOrderLine object| Field | Type | Description |
|---|---|---|
id, product_id, variation_id | integer | Identifiers for the sales-order line and linked catalog records. |
product_label | string | Computed product display label, including variation labels when available. |
quantity, so_quantity_invoiced, quantity_remaining | number | Ordered quantity, already invoiced quantity, and remaining quantity for the line. |
unit_price, unit_price_inc_tax | number | null | Stored unit prices excluding and including tax. |
line_tax | object | null | Optional tax summary with id, name, and amount. |
SalesOrderDetail object| Field | Type | Description |
|---|---|---|
id | integer | Sales-order transaction id. |
invoice_no, document | string | null | Stored reference fields for the order. |
transaction_date | string | null | ISO-8601 transaction timestamp. |
status, payment_status, shipping_status | string | null | Order workflow, payment state, and shipping state. |
final_total, total_before_tax, tax_amount, discount_amount | number | null | Order totals from the transaction header. |
additional_notes | string | null | Saved order notes. |
contact | object | null | Customer 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 | Order-level tax summary with id, name, amount, and is_tax_group. |
lines | array<SalesOrderLine> | Parent sales-order lines sorted by line id. |
| Field | Type | Description |
|---|---|---|
data | SalesOrderDetail | The requested sales-order payload. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The sales order was returned successfully. | { "data": { ... } } or CSV download. |
403 | The token user lacks the required sales-order permission or sales orders are disabled in POS settings. | { "message": string } |
404 | The sales-order id does not exist in the visible query. | { "message": "Not found" } |
422 | The query string failed validation. | Laravel validation JSON. |
Creates a new sales order. Fulfilment happens later when a normal sale references the order.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/sales-orders |
| Permission | so.create. |
| Module requirement | The business must have POS settings -> Enable sales order enabled. |
| Subscription and quota | The business must be subscribed and still have invoice quota available. |
| Created transaction | The controller creates type = sales_order, status = ordered, and is_direct_sale = 1, then writes the sales-order lines and activity log. |
| Product rule | Combo products are rejected with 422. |
| Success response | 201 with the created id and invoice number. |
| Field | Type | Required | Description |
|---|---|---|---|
contact_id | integer | Yes | Customer contact id. The contact must belong to the business and have type customer or both. |
location_id | integer | Yes | Business location id. |
transaction_date | string | Yes | Order date accepted by Laravel date validation and normalized with uf_date(..., true). |
tax_rate_id | integer | null | No | Optional business tax-rate id. |
discount_type | string | null | No | fixed or percentage. Defaults to fixed when omitted. |
discount_amount | number | null | No | Optional order-level discount amount. |
sale_note | string | null | No | Optional internal note saved on the transaction. |
invoice_no | string | null | No | Optional custom invoice/reference number, up to 191 characters. |
shipping_details, shipping_address | string | null | No | Optional shipping text fields. |
shipping_status | string | null | No | Optional shipping status string up to 64 characters. |
shipping_charges | number | null | No | Optional shipping charge total. |
products | array<SalesOrderCreateLine> | Yes | At least one order line. |
SalesOrderCreateLine object| Field | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | Product id. |
variation_id | integer | Yes | Variation id for the selected product. |
quantity | number | Yes | Ordered quantity. |
unit_price | number | Yes | Unit price excluding tax. |
unit_price_inc_tax | number | Yes | Unit price including tax. |
item_tax | number | Yes | Line tax amount. |
line_discount_type | string | null | No | fixed or percentage. |
line_discount_amount | number | null | No | Optional line-level discount amount. |
| Field | Type | Description |
|---|---|---|
message | string | Localized create-success message. |
data.id | integer | The created sales-order id. |
data.invoice_no | string | null | The stored invoice/reference number. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The sales order was created successfully. | { "message": string, "data": { "id": integer, "invoice_no": string | null } } |
402 | The business is not subscribed or has reached invoice quota. | { "message": string } |
403 | Demo mode is active, the token user lacks so.create, or sales orders are disabled in POS settings. | { "message": string } |
422 | The request body failed validation, the invoice-total calculation rejected the product payload, or a combo product was submitted. | Laravel validation JSON or { "message": string }. |
500 | The sales-order create transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Updates an existing visible sales order and rewrites its line set.
| Property | Value |
|---|---|
| Method | PUT or PATCH |
| Path | /api/v1/integration/sales-orders/{id} |
| Permission | so.update. |
| Module and subscription | Sales orders must be enabled in POS settings and the business must be subscribed. |
| Visibility | The target row must be visible through the same location and own/all rules used by List sales orders. |
| Edit blockers | The order cannot be updated when a linked return exists or when the transaction falls outside the business edit window defined by transaction_edit_days. |
| Request body | Same payload as Create sales order, plus optional products.*.transaction_sell_lines_id for existing lines. |
| Success response | 200 with the updated id and invoice number. |
| Field | Type | Required | Description |
|---|---|---|---|
products.*.transaction_sell_lines_id | integer | No | Existing parent sales-order line id to update in place. |
| Field | Type | Description |
|---|---|---|
message | string | Localized update-success message. |
data.id | integer | The updated sales-order id. |
data.invoice_no | string | null | The saved invoice/reference number after the update. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The sales order was updated successfully. | { "message": string, "data": { "id": integer, "invoice_no": string | null } } |
402 | The business is not subscribed. | { "message": string } |
403 | Demo mode is active, the token user lacks so.update, or sales orders are disabled in POS settings. | { "message": string } |
404 | The sales-order id does not exist in the visible query. | { "message": "Not found" } |
422 | A linked return exists, the order is outside the allowed edit window, the request body failed validation, the invoice-total calculation rejected the product payload, or a combo product was submitted. | Laravel validation JSON or { "message": string }. |
500 | The sales-order update transaction failed unexpectedly. | { "message": string } |
Deletes a visible sales order by delegating to the normal sell-delete pipeline.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/v1/integration/sales-orders/{id} |
| Permission | so.delete. |
| Module requirement | The business must have sales orders enabled in POS settings. |
| Delete behavior | The controller first verifies that the row is visible, then delegates to TransactionUtil::deleteSale(). |
| Success response | 200 with the deleted id. |
| Field | Type | Description |
|---|---|---|
message | string | Delete result message returned by the delete pipeline. |
data.id | integer | The deleted sales-order id. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The sales order was deleted successfully. | { "message": string, "data": { "id": integer } } |
403 | Demo mode is active, the token user lacks so.delete, or sales orders are disabled in POS settings. | { "message": string } |
404 | The sales-order id does not exist in the visible query. | { "message": "Not found" } |
422 | The delete pipeline rejected the sales order because of business rules such as linked returns or other delete blockers. | { "message": string } |
Updates the workflow status on a visible sales order.
| Property | Value |
|---|---|
| Method | PUT or PATCH |
| Path | /api/v1/integration/sales-orders/{id}/status |
| Permission | Business admin only. |
| Module requirement | The business must have sales orders enabled in POS settings. |
| Demo mode | Returns 403 in demo environments. |
| Allowed statuses | ordered, partial, or completed. |
| Fulfilment note | Status changes do not invoice the order. Fulfilment still happens when a normal sell references sales_order_ids. |
| Success response | 200 with the updated id and status. |
| Field | Type | Required | Description |
|---|---|---|---|
status | string | Yes | New workflow status: ordered, partial, or completed. |
| Field | Type | Description |
|---|---|---|
message | string | Localized success message. |
data.id | integer | The sales-order id. |
data.status | string | The saved workflow status. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The sales-order status was updated successfully. | { "message": string, "data": { "id": integer, "status": string } } |
403 | Demo mode is active, the token user is not a business admin, or sales orders are disabled in POS settings. | { "message": string } |
404 | The sales-order id does not exist in the visible query. | { "message": "Not found" } |
422 | The request body failed validation. | Laravel validation JSON. |
Lists types of service configured for the current business.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/types-of-service |
| Permission | access_types_of_service. |
| Module requirement | The business must have the types_of_service module enabled. |
| Search behavior | When q is present it takes precedence over the legacy search parameter. |
| CSV behavior | format=csv streams all matching rows with a UTF-8 BOM and ignores page and per_page. |
| Success response | 200 with paginated TypesOfServiceRow 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 | Preferred search parameter. When sent, it must be at least 2 characters and matches name, description, or numeric id. |
search | string | No | Legacy search parameter used only when q is absent. |
sort | string | No | name, packing_charge_type, or created_at. Defaults to name. |
direction | string | No | asc or desc. Defaults to asc. |
format | string | No | json (default) or csv. |
TypesOfServiceRow object| Field | Type | Description |
|---|---|---|
id | integer | Type-of-service id. |
name | string | Configured type-of-service name. |
description | string | null | Optional long-form description. |
location_price_group | array | object | null | Location-to-selling-price-group mapping, decoded from stored JSON when needed. |
packing_charge | number | null | Packing charge value normalized with the business number format. |
packing_charge_type | string | null | fixed or percent. |
enable_custom_fields | boolean | Whether custom fields are enabled for this service type. |
created_at, updated_at | string | null | ISO-8601 timestamps. |
| Field | Type | Description |
|---|---|---|
data | array<TypesOfServiceRow> | 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 type-of-service list was returned successfully. | { "data": [...], "meta": { ... } } or CSV download. |
403 | The token user lacks access_types_of_service or the module is disabled. | { "message": string } |
422 | The query string failed validation or q was shorter than 2 characters. | Laravel validation JSON or { "message": string }. |
500 | The list query failed unexpectedly. | { "message": "Could not list types of service" } |
Returns one type of service by id.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/integration/types-of-service/{id} |
| Permission | Same permission and module rules as List types of service. |
| CSV behavior | format=csv streams one UTF-8 BOM row whose data_json column matches the JSON data payload. |
| Success response | 200 with one TypesOfServiceRow object. |
| Field | Type | Description |
|---|---|---|
data | TypesOfServiceRow | The requested type-of-service payload. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The type of service was returned successfully. | { "data": { ... } } or CSV download. |
403 | The token user lacks access_types_of_service or the module is disabled. | { "message": string } |
404 | The type-of-service id does not exist for this business. | { "message": "Not found" } |
422 | The query string failed validation. | Laravel validation JSON. |
Creates a new type of service for the current business.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/integration/types-of-service |
| Permission | access_types_of_service. |
| Module requirement | The business must have the types_of_service module enabled. |
| Demo mode | Returns 403 in demo environments. |
| Default behavior | When packing_charge is omitted it is stored as 0. When enable_custom_fields is omitted it defaults to false. |
| Success response | 201 with one TypesOfServiceRow object. |
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Service-type name, up to 255 characters. |
description | string | null | No | Optional description, up to 5000 characters. |
location_price_group | array | object | null | No | Optional mapping where keys are location ids and values are selling-price-group ids. |
packing_charge_type | string | null | No | fixed or percent. |
packing_charge | string | number | null | No | Optional charge value parsed with Util::num_uf(). |
enable_custom_fields | boolean | No | Whether custom fields should be enabled for this service type. |
| Field | Type | Description |
|---|---|---|
data | TypesOfServiceRow | The created type-of-service payload. |
| Status | When it happens | Response shape |
|---|---|---|
201 | The type of service was created successfully. | { "data": { ... } } |
403 | Demo mode is active, the token user lacks access_types_of_service, or the module is disabled. | { "message": string } |
422 | The request body failed validation. | Laravel validation JSON. |
500 | The type-of-service create transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Updates an existing type of service for the current business.
| Property | Value |
|---|---|
| Method | PUT or PATCH |
| Path | /api/v1/integration/types-of-service/{id} |
| Permission | access_types_of_service. |
| Module requirement | The business must have the types_of_service module enabled. |
| Demo mode | Returns 403 in demo environments. |
| Update behavior | Uses the same payload as Add type of service. When enable_custom_fields is omitted the existing value is left unchanged. |
| Success response | 200 with one TypesOfServiceRow object. |
| Field | Type | Required | Description |
|---|---|---|---|
| All Add type of service fields | mixed | See above | The same request-body schema is reused for updates. |
| Field | Type | Description |
|---|---|---|
data | TypesOfServiceRow | The updated type-of-service payload. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The type of service was updated successfully. | { "data": { ... } } |
403 | Demo mode is active, the token user lacks access_types_of_service, or the module is disabled. | { "message": string } |
404 | The type-of-service id does not exist for this business. | { "message": "Not found" } |
422 | The request body failed validation. | Laravel validation JSON. |
500 | The type-of-service update transaction failed unexpectedly. | { "message": "something_went_wrong" } |
Deletes a type of service when it is not referenced by any transaction in the business.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/v1/integration/types-of-service/{id} |
| Permission | access_types_of_service. |
| Module requirement | The business must have the types_of_service module enabled. |
| Demo mode | Returns 403 in demo environments. |
| Delete blocker | The delete is rejected when any transaction in the business already references transactions.types_of_service_id = {id}. |
| Success response | 200 with the deleted id. |
| Field | Type | Description |
|---|---|---|
message | string | Localized delete-success message. |
data.id | integer | The deleted type-of-service id. |
| Status | When it happens | Response shape |
|---|---|---|
200 | The type of service was deleted successfully. | { "message": string, "data": { "id": integer } } |
403 | Demo mode is active, the token user lacks access_types_of_service, or the module is disabled. | { "message": string } |
404 | The type-of-service id does not exist for this business. | { "message": "Not found" } |
422 | The service type is already referenced by at least one transaction. | { "message": string } |
500 | The type-of-service delete transaction failed unexpectedly. | { "message": "something_went_wrong" } |