developer_tools.public_docs_home developer_tools.public_api_reference
developer_tools.subtitle
developer_tools.nav_module_sells
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" } |