{"openapi":"3.0.3","info":{"title":"POS PRO-360 Mobile API (v2)","version":"0.0.1","description":"API for the OHMS POS mobile app: billing, returns, stock take, purchase orders, transfers, shifts and admin approvals. Every call except sign-in needs a bearer token, and the branch comes from that token."},"servers":[{"url":"/api/v2","description":"Mobile app API v2"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"JWT from POST /auth/login"}},"schemas":{"ApiResponse":{"type":"object","properties":{"success":{"type":"boolean"},"data":{},"message":{"type":"string"}}},"LoginBody":{"type":"object","required":["login_id","password"],"properties":{"login_id":{"type":"string"},"password":{"type":"string"},"store_code":{"type":"string"}}},"StoreCreateBody":{"type":"object","properties":{"name":{"type":"string"},"address":{"type":"string"},"phone":{"type":"string"},"timezone":{"type":"string","default":"UTC"},"is_active":{"type":"boolean","default":true}}}}},"tags":[{"name":"Auth","description":"Sign in and keep the session alive. Send the access token on every other call as `Authorization: Bearer <token>`. The branch comes from the token, so most calls never ask for one."},{"name":"Home","description":"What the home screen and the rest of the app need at start-up."},{"name":"Products","description":"Find products. Scanning a barcode uses `GET /pos/products/by-barcode/{barcode}`; search uses `GET /pos/products`."},{"name":"Models","description":"Lists for pickers, such as suppliers, categories and brands."},{"name":"Sales","description":"Bill, take payment, send the receipt, and refund."},{"name":"Stock take","description":"Count the shelves over one or more days, review the differences, and submit for approval. Stock changes only when an admin approves."},{"name":"Purchase orders","description":"Pick a supplier, build the order, and send it to admin."},{"name":"Transfers","description":"Move stock between branches. A transfer goes `requested` (optional), then `in_transit`, then `received` or `discrepancy`."},{"name":"Reports and shift","description":"Branch reports, and opening and closing the cash drawer."},{"name":"Admin","description":"For admin users only (role 1). Other users get `403`."}],"paths":{"/":{"get":{"tags":[],"summary":"API root","description":"Returns API message and version.","responses":{"200":{"description":"{ message, version }"}}}},"/version":{"get":{"tags":[],"summary":"API version","description":"Returns package version.","responses":{"200":{"description":"{ version }"}}}},"/auth/login":{"post":{"tags":["Auth"],"summary":"Login","description":"Authenticate with login_id and password. Returns access token and refresh token.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginBody"}}}},"responses":{"200":{"description":"Login successful"},"401":{"description":"Invalid credentials"}}}},"/auth/refresh-token":{"post":{"tags":["Auth"],"summary":"Refresh access token","description":"Exchange refresh token for a new access token.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"}}}}}},"responses":{"200":{"description":"New access token"},"401":{"description":"Invalid token"}}}},"/auth/logout":{"post":{"tags":["Auth"],"summary":"Logout","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Logout successful"}},"description":"Ends the session. Clear the saved tokens on the device afterwards."}},"/auth/me":{"get":{"tags":["Auth"],"summary":"Current user","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"User info"},"401":{"description":"Unauthorized"}},"description":"The signed-in user with their role and branch. Use it to decide which screens to show."}},"/master/suppliers":{"get":{"tags":["Models"],"summary":"List suppliers (store-scoped)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Suppliers (id, name)"},"401":{"description":"Unauthorized"}},"description":"Suppliers for this branch. Fills the supplier picker on a new purchase order."}},"/master/categories":{"get":{"tags":["Models"],"summary":"List product categories (store-scoped)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Categories (id, name, parent_id)"},"401":{"description":"Unauthorized"}},"description":"Product categories for this branch, for filters and pickers."}},"/master/brands":{"get":{"tags":["Models"],"summary":"List brands (store-scoped)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Brands (id, name, is_active)"},"401":{"description":"Unauthorized"}},"description":"Product brands for this branch, for filters and pickers."}},"/products/next-code":{"get":{"tags":["Products"],"summary":"Get next product code","description":"Returns the next product code and increments the sequence. Call only when saving a new product. No product id required; uses retail_sequence (module PRODUCT).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Next code allocated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"code":{"type":"string"}}},"message":{"type":"string"}}}}}},"500":{"description":"Sequence not found or server error"}}}},"/products/peek-next-code":{"get":{"tags":["Products"],"summary":"Peek next product code","description":"Returns what the next product code would be without incrementing the sequence. Use for preview (e.g. in form). No product id required.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Preview of next code","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"code":{"type":"string"}}},"message":{"type":"string"}}}}}},"500":{"description":"Sequence not found or server error"}}}},"/products/current-code":{"get":{"tags":["Products"],"summary":"Get current product code","description":"Returns the last allocated product code from the sequence (current value). No product id required.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Current (last allocated) code","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"code":{"type":"string"}}},"message":{"type":"string"}}}}}},"500":{"description":"Sequence not found or server error"}}}},"/products/next-variant-code":{"get":{"tags":["Products"],"summary":"Get next variant code","description":"Returns the next variant code and increments the sequence (module VARIANT).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Next variant code"},"500":{"description":"Sequence error"}}}},"/products/peek-next-variant-code":{"get":{"tags":["Products"],"summary":"Peek next variant code","description":"Preview next variant code without incrementing.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Preview next variant code"},"500":{"description":"Sequence error"}}}},"/products/current-variant-code":{"get":{"tags":["Products"],"summary":"Get current variant code","description":"Returns the last allocated variant code.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Current variant code"},"500":{"description":"Sequence error"}}}},"/products/export":{"get":{"tags":["Products"],"summary":"Export products","description":"Export products as CSV.","security":[{"bearerAuth":[]}],"parameters":[{"name":"search","in":"query","schema":{"type":"string"}},{"name":"search_type","in":"query","schema":{"type":"string","enum":["all","barcode","code","name"]}}],"responses":{"200":{"description":"CSV file"},"401":{"description":"Unauthorized"}}}},"/products":{"get":{"tags":["Products"],"summary":"List products","description":"Paginated list of products for the store. Supports search by name, code (sku), or barcode.","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1},"description":"Page number"},{"name":"limit","in":"query","schema":{"type":"integer","default":10},"description":"Items per page"},{"name":"search","in":"query","schema":{"type":"string"},"description":"Search term"},{"name":"search_type","in":"query","schema":{"type":"string","enum":["all","barcode","code","name"]},"description":"Search field"},{"name":"category_id","in":"query","schema":{"type":"integer"},"description":"Filter by category"},{"name":"status","in":"query","schema":{"type":"string"},"description":"Filter by status (e.g. active)"}],"responses":{"200":{"description":"Paginated products (data, total, page, limit, totalPages)"},"401":{"description":"Unauthorized"}}}},"/products/{id}":{"get":{"tags":["Products"],"summary":"Get product by ID","description":"Returns full product detail by numeric id. Do not use literal paths like peek-next-code as id.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"},"description":"Product ID"}],"responses":{"200":{"description":"Product detail"},"400":{"description":"Invalid id"},"404":{"description":"Not found"}}}},"/pos/products":{"get":{"tags":["Products"],"summary":"List POS products","description":"List products for POS (store context).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Products list"},"401":{"description":"Unauthorized"}}}},"/pos/products/{id}":{"get":{"tags":["Products"],"summary":"Get POS product by ID","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Product"},"404":{"description":"Not found"}},"description":"One product with its till price and stock on hand."}},"/pos/products/by-barcode/{barcode}":{"get":{"tags":["Products"],"summary":"Get product by barcode","description":"Lookup product by barcode (barcode or additional_barcodes).","security":[{"bearerAuth":[]}],"parameters":[{"name":"barcode","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Product for POS (id, name, sku, barcode, price, stock)"},"404":{"description":"Not found"}}}},"/products/setting/taxes":{"get":{"tags":["Products"],"summary":"List taxes (paginated)","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated taxes"}},"description":"Tax rates set up for this branch."}},"/products/setting/brands":{"get":{"tags":["Products"],"summary":"List brands (paginated)","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated brands"}},"description":"Brands, a page at a time. Use this for long lists; `/master/brands` suits short pickers."}},"/products/setting/units":{"get":{"tags":["Products"],"summary":"List units (paginated)","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated units"}},"description":"Units of measure, such as piece or kilogram."}},"/products/setting/categories":{"get":{"tags":["Products"],"summary":"List product categories (paginated)","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated categories"}},"description":"Product categories, a page at a time."}},"/products/setting/variant-attributes":{"get":{"tags":["Products"],"summary":"List variant attributes","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated variant attributes"}},"description":"Attributes that define product variants, such as size or colour."}},"/products/setting/variant-attributes/{attributeId}/values":{"get":{"tags":["Products"],"summary":"List attribute values","security":[{"bearerAuth":[]}],"parameters":[{"name":"attributeId","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Values list"}},"description":"The allowed values for one variant attribute."}},"/products/setting/sequences":{"get":{"tags":["Products"],"summary":"List sequences","description":"List all retail_sequence rows (e.g. PRODUCT, STORE, VARIANT) for code generation.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Array of { module, number, prefix, suffix, column_length, is_append_prefix, is_append_suffix }"},"500":{"description":"Server error"}}}},"/products/setting/payment-settings":{"get":{"tags":["Products"],"summary":"List payment settings","description":"Payment types from `retail_payment_settings` (store-scoped rows + global `store_id` null).","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"search","in":"query","schema":{"type":"string"}},{"name":"active_only","in":"query","description":"If true, return only rows where is_active is true (recommended for POS).","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Paginated payment settings"},"401":{"description":"Unauthorized"}}}},"/pos/dashboard":{"get":{"tags":["Home"],"summary":"Sales summary","description":"Sales totals for the signed-in user's branch. Powers the home screen.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/pos/pos-settings":{"get":{"tags":["Home"],"summary":"Till settings","description":"Till configuration for the branch, including the default payment type. The payment types themselves come from `GET /products/setting/payment-settings`; build the payment screen from that list and never hardcode it.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/stores":{"get":{"tags":["Home"],"summary":"List branches","description":"Branches the app can name in transfers and requests.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/pos/sales":{"post":{"tags":["Sales"],"summary":"Complete a sale","description":"Records a finished bill and takes the units out of stock. Send a unique `clientTxnId`: if the network drops and the app retries, the same bill is returned instead of a second one, and the response has the header `Idempotent-Replay: true`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["salesOrderNo","lineSubtotal","discountPercent","discountAmount","taxAmount","roundOff","serviceFeeAmount","grandTotal","lines","payments"],"properties":{"salesOrderNo":{"type":"string","description":"Bill number"},"clientTxnId":{"type":"string","description":"Unique id the app generates per bill. Makes retries safe"},"lineSubtotal":{"type":"number","description":"Sum of the lines"},"discountPercent":{"type":"number","description":"Bill discount percent"},"discountAmount":{"type":"number","description":"Bill discount amount"},"taxAmount":{"type":"number","description":"GST on the bill"},"roundOff":{"type":"number","description":"Cash rounding"},"serviceFeeAmount":{"type":"number","description":"Service fee, or 0"},"grandTotal":{"type":"number","description":"Amount the customer pays"},"lines":{"type":"array","items":{"type":"object","required":["lineNo","productName","qty","unitPrice","lineDiscount","lineTotal"],"properties":{"lineNo":{"type":"integer","description":"Line number from 1"},"productId":{"type":"integer","description":"Product id"},"productName":{"type":"string","description":"Name printed on the receipt"},"qty":{"type":"number","description":"Units"},"unitPrice":{"type":"number","description":"Price per unit"},"lineDiscount":{"type":"number","description":"Line discount, or 0"},"lineTotal":{"type":"number","description":"Line total"}}}},"payments":{"type":"array","items":{"type":"object","required":["lineNo","paymentName","amount"],"properties":{"lineNo":{"type":"integer","description":"Payment number from 1"},"paymentName":{"type":"string","description":"Payment type name from till settings"},"amount":{"type":"number","description":"Amount applied to the bill, excluding change. Payments must add up to the grand total"},"paymentSettingId":{"type":"integer","description":"Payment setting id"}}}}}}}}},"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}},"get":{"tags":["Sales"],"summary":"List bills","description":"Past bills for the branch. Use it to find the bill a customer is returning.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/pos/sales/detail":{"get":{"tags":["Sales"],"summary":"Bill detail","description":"One bill with its lines and payments, found by bill number. `saleId` and each line's `saleLineId` are what `POST /mobile/returns` needs.","security":[{"bearerAuth":[]}],"parameters":[{"name":"sales_order_no","in":"query","description":"Required. The bill number","schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/pos/send-receipt-email":{"post":{"tags":["Sales"],"summary":"Email a receipt","description":"Emails the tax invoice for a completed bill to the customer.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/returns":{"post":{"tags":["Sales"],"summary":"Refund against a bill","description":"Refunds units from an existing bill. The amount is worked out from the bill, including its discount and GST, so it is never typed in. The refund goes to the first payment type on the bill, and the refunds on a bill can never add up to more than was paid.\n\nReturning more units than are left on a line gives `409`. Retrying with the same `client_txn_id` returns the original refund instead of paying twice. `restock: true` puts the units back on the shelf; leave it false for damaged goods.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sale_id","client_txn_id","reason","lines"],"properties":{"sale_id":{"type":"integer","description":"Bill id"},"client_txn_id":{"type":"string","description":"Unique id the app generates per refund"},"reason":{"type":"string","description":"Why it came back, such as Damaged"},"restock":{"type":"boolean","description":"Put the units back in stock. Default false"},"lines":{"type":"array","items":{"type":"object","required":["sale_line_id","qty"],"properties":{"sale_line_id":{"type":"integer","description":"Line id from the bill"},"qty":{"type":"number","description":"Units being returned"}}}}}}}}},"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}},"get":{"tags":["Sales"],"summary":"List refunds","description":"The latest 100 refunds for the branch, newest first.","security":[{"bearerAuth":[]}],"parameters":[{"name":"sale_id","in":"query","description":"Only refunds for this bill","schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/stock-takes":{"get":{"tags":["Stock take"],"summary":"List counts","description":"The latest 50 counts for the branch with how many products each has counted. Status is `open`, `submitted`, `approved` or `rejected`.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}},"post":{"tags":["Stock take"],"summary":"Start a count","description":"Opens a count that can stay open across several days. A branch can have one active count at a time. Starting another while one is open or waiting for approval gives `409`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Name staff will recognise, such as Monthly count November"}}}}}},"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/stock-takes/{id}":{"get":{"tags":["Stock take"],"summary":"Count detail and review","description":"Every counted line with its stock snapshot and variance, plus a summary: products counted, lines short, lines over, and the net variance valued at cost price.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/stock-takes/{id}/lines":{"put":{"tags":["Stock take"],"summary":"Record a shelf count","description":"Saves the count for one product. The system quantity is captured at that moment and kept with the line, so sales on later days do not create false variances. Counting the same product again replaces the earlier count. Stock does not change until an admin approves.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["product_id","counted_qty"],"properties":{"product_id":{"type":"integer","description":"Product id, from the barcode lookup"},"counted_qty":{"type":"number","description":"Units on the shelf, zero or more"}}}}}},"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/stock-takes/{id}/submit":{"post":{"tags":["Stock take"],"summary":"Submit for approval","description":"Closes counting and sends the count to admin. Lines cannot be changed afterwards.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/purchase/orders":{"get":{"tags":["Purchase orders"],"summary":"List purchase orders","description":"Purchase orders for the branch.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}},"post":{"tags":["Purchase orders"],"summary":"Create a purchase order","description":"Saves a draft order for a supplier. Pick the supplier from `GET /master/suppliers`.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/purchase/orders/{id}":{"get":{"tags":["Purchase orders"],"summary":"Purchase order detail","description":"One order with its lines.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/purchase/orders/{id}/submit":{"post":{"tags":["Purchase orders"],"summary":"Submit to admin","description":"Sends the order to admin, who verifies it and raises the goods receipt.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/purchase/suppliers/{supplierId}/products/{productId}/previous-price":{"get":{"tags":["Purchase orders"],"summary":"Last price paid","description":"What this supplier last charged for this product. Use it to prefill the rate on a new order.","security":[{"bearerAuth":[]}],"parameters":[{"name":"supplierId","in":"path","required":true,"schema":{"type":"string"}},{"name":"productId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/transfers":{"get":{"tags":["Transfers"],"summary":"List transfers","description":"`incoming`: on the way to this branch. `outgoing`: sent by this branch. `requests`: waiting for an answer, either way.","security":[{"bearerAuth":[]}],"parameters":[{"name":"direction","in":"query","description":"incoming, outgoing or requests. Default incoming","schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}},"post":{"tags":["Transfers"],"summary":"Send stock to a branch","description":"Takes the units out of this branch immediately and marks the transfer `in_transit`. The other branch adds them to its stock when it receives them. Products are matched between branches by SKU; a product with no match gives `400`, and sending more than is on hand gives `409`. Retrying with the same `client_txn_id` returns the first transfer, so stock never moves twice.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["client_txn_id","to_store_id","lines"],"properties":{"client_txn_id":{"type":"string","description":"Unique id the app generates per transfer"},"to_store_id":{"type":"integer","description":"Branch receiving the stock"},"notes":{"type":"string","description":"Note for the other branch"},"lines":{"type":"array","items":{"type":"object","required":["product_id","qty"],"properties":{"product_id":{"type":"integer","description":"Product id in the store sending this request"},"qty":{"type":"number","description":"Units, above zero"}}}}}}}}},"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/transfers/requests":{"post":{"tags":["Transfers"],"summary":"Request stock from a branch","description":"Asks another branch for stock. Nothing moves until that branch sends it. Retrying with the same `client_txn_id` returns the first request.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["client_txn_id","from_store_id","lines"],"properties":{"client_txn_id":{"type":"string","description":"Unique id the app generates per request"},"from_store_id":{"type":"integer","description":"Branch being asked"},"urgent":{"type":"boolean","description":"Flag as urgent"},"notes":{"type":"string","description":"Note for the other branch"},"lines":{"type":"array","items":{"type":"object","required":["product_id","qty"],"properties":{"product_id":{"type":"integer","description":"Product id in the store sending this request"},"qty":{"type":"number","description":"Units, above zero"}}}}}}}}},"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/transfers/{id}":{"get":{"tags":["Transfers"],"summary":"Transfer detail","description":"One transfer or request with its lines. Only the two branches involved can see it. Use the line ids to send or receive.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/transfers/{id}/dispatch":{"post":{"tags":["Transfers"],"summary":"Approve and send a request","description":"Answers a request addressed to this branch. Send fewer units than were asked if that is all you hold. The units leave this branch and the request becomes a transfer `in_transit`.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["lines"],"properties":{"lines":{"type":"array","items":{"type":"object","required":["line_id","qty"],"properties":{"line_id":{"type":"integer","description":"Line id"},"qty":{"type":"number","description":"Units to send. Zero skips the line"}}}}}}}}},"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/transfers/{id}/reject":{"post":{"tags":["Transfers"],"summary":"Reject a request","description":"Declines a pending request, or withdraws one this branch made. No stock moves.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/transfers/{id}/receive":{"post":{"tags":["Transfers"],"summary":"Receive a transfer","description":"Scan-in result for a transfer on its way here. Give the received quantity for every line. Received units join this branch's stock.\n\nIf any line is short, the transfer becomes `discrepancy` and the missing units go back on the sender's stock, logged against the transfer. Otherwise it becomes `received`.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["lines"],"properties":{"lines":{"type":"array","items":{"type":"object","required":["line_id","received_qty"],"properties":{"line_id":{"type":"integer","description":"Line id"},"received_qty":{"type":"number","description":"Units actually received, no more than were sent"}}}}}}}}},"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/pos/sales-reports":{"get":{"tags":["Reports and shift"],"summary":"Sales reports","description":"Branch sales for the reports screen.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/shifts/current":{"get":{"tags":["Reports and shift"],"summary":"Current shift","description":"The open shift with the cash expected in the drawer: opening float, plus cash sales, minus cash refunds since it opened. `data` is null when no shift is open.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/shifts/open":{"post":{"tags":["Reports and shift"],"summary":"Open a shift","description":"Starts a shift for the branch. A second open shift gives `409`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":[],"properties":{"opening_float":{"type":"number","description":"Cash in the drawer at the start. Default 0"}}}}}},"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/shifts/close":{"post":{"tags":["Reports and shift"],"summary":"Close the shift","description":"Records the counted cash and the variance against what was expected. A variance is kept with the shift for admin; it is not corrected here.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["counted_cash"],"properties":{"counted_cash":{"type":"number","description":"Cash counted in the drawer"},"notes":{"type":"string","description":"Explanation of any difference"}}}}}},"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/admin/branches":{"get":{"tags":["Admin"],"summary":"All branches at a glance","description":"Admin only. Per branch: sales and bills since the start time, active counts, incoming transfers, and shift variance.","security":[{"bearerAuth":[]}],"parameters":[{"name":"from","in":"query","description":"ISO start time. Default midnight UTC today","schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/admin/approvals":{"get":{"tags":["Admin"],"summary":"Approval queue","description":"Admin only. Counts and purchase orders waiting for a decision, across all branches, oldest first.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/admin/stock-takes/{id}/approve":{"post":{"tags":["Admin"],"summary":"Approve a count","description":"Admin only. Applies each line's variance to stock in one step: all lines post, or none do. Stock can go negative if sales since the count took it below the correction; that is shown on the next count.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}},"/mobile/admin/stock-takes/{id}/reject":{"post":{"tags":["Admin"],"summary":"Reject a count","description":"Admin only. Closes the count without changing stock. The branch can then start a new one.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success. The result is in `data`."},"400":{"description":"The request is missing a field or has an invalid value. `message` says which."},"401":{"description":"Missing or expired token. Refresh it with `POST /auth/refresh-token`."},"403":{"description":"This user is not allowed to do this."},"404":{"description":"Not found in this branch."},"409":{"description":"The record is not in the right state for this, or there is not enough stock. `message` says why."}}}}}}