Payments, Delivery and Discounts API
Payment methods, payment gateways, banks, delivery methods and charges, discount coupons and POS offers, as the CloudPOS POS app loads and applies them.
These endpoints fill the Payment (F5), Delivery (F4) and Discount & Coupon (F10) steps of Finalize Order, and the offer prices shown in Billing.
All paths are relative to {{server}}. Send X-Tenant: <your-tenant-key> on every call, plus Authorization: Bearer <access-token> where marked. The app adds store_id and the locale (?locale= and Accept-Language).
Quick reference
Method | Path | Auth | Used for |
|---|---|---|---|
GET |
| tenant | Payment methods in checkout and payment filters |
GET |
| bearer | Online gateways (shown on some receipt layouts) |
GET |
| tenant | Company banks and accounts (bank info on prints) |
GET |
| tenant | Delivery methods and their prices |
GET |
| tenant | Weight-based charge (web storefront; the POS calculates its own charge) |
GET |
| tenant | Coupons the POS can validate offline |
GET |
| tenant | Server check of a coupon code |
GET |
| bearer | Product and category offers (when POS_OFFERS is on) |
GET /api/v1/master-data-values?code=PAYMENT_METHOD
Purpose: the list of payment methods. The ids are what orders store.
Screens: Payment dialog (Finalize Order F5, Multi-Payment), Payment Status action, Cancel / refund pickers, report and list filters, Offline Data "Payment methods".
Param | Example | Notes |
|---|---|---|
|
| Required. A missing code returns |
|
| Sent by the app. |
|
| Translated |
Captured response for store 2 (trimmed: translations shortened, 1 more method):
{
"status": "success",
"message": "Filtered Master Data Values",
"meta": { "available_languages": ["ar", "en", "…"], "requested_language": "*" },
"data": [
{
"id": 7973,
"master_data_id": 16,
"value": "CARD",
"description": "card",
"code": "CARD",
"label": "card",
"translations": [
{ "locale": "ar", "key": "description", "value": "بطاقة" },
{ "locale": "ml", "key": "description", "value": "കാർഡ്" }
]
},
{
"id": 7974,
"master_data_id": 16,
"value": "CASH",
"description": "cash",
"code": "CASH",
"label": "cash",
"translations": [
{ "locale": "ar", "key": "description", "value": "نقدًا" },
{ "locale": "ml", "key": "description", "value": "ക്യാഷ്" }
]
}
]
}So in the demo company CARD is id 7973 and CASH is id 7974. Seeded values are COD, ONLINE, CHEQUE, UPI, CASH and CARD; each company can switch them off or add its own.
Fields the app reads
Field | Default | Effect |
|---|---|---|
| Stored on the order ( | |
| Machine code: CASH, CARD, UPI, COD, ONLINE, DEBIT or any custom code. | |
| Button label in the user's language. | |
| true | Hidden when false. |
| 0 | Order of the buttons (then by label). |
| Icon. | |
|
|
|
| false | Asks for a transaction / reference number. |
Credit is always offered. If the list has no credit method, the app adds Credit (code DEBIT) at the end.
Payment rules in checkout
- Split payment (Multi): each method gets an amount. The order is sent with
payment_method: [ids]andpaid_methods: [{method, amount}]. - Change: when more is paid than the bill, the change is taken off the first cash (or COD) entry before sending. Demo: card 400 + cash 300 for 658.00 → 42.00 change → stored as card 400 + cash 258.
- Excess Amount switch: on = the extra is kept as customer credit (
to_customer_credit: true); off = returned as change. - Sell on Credit: the unpaid part is posted to the customer's balance (company prop CUSTOMER_MAX_CREDIT_LIMIT, default 1000, is the credit limit).
- COD: the order is confirmed and invoiced, payment stays pending until delivery; a COD allocation must cover the whole remaining total and cannot create customer credit.
- Default method: company prop DEFAULT_PAYMENT_METHOD (seeded CASH) is preselected.
GET /api/v1/payment-gateway/list-payment-gateways
Purpose: online payment gateways configured for the company. The app reads them for receipt layouts that print payment details.
Captured response (no gateway configured in the demo):
{ "status": "success", "message": "Payment Gateways listed successfully", "data": [] }GET /api/v1/get-banks
Purpose: company banks with their accounts. Loaded during start-up sync ("banks" step) and used when a print template shows bank information (showBankInfo).
Captured response (account details replaced with placeholders):
{
"status": "success",
"message": "Bank List",
"data": [
{
"id": 2,
"bank_name": "Al Rajhi Bank",
"bank_code": "RJHI",
"head_office": "Riyadh",
"status": 1,
"company_id": 2,
"bank_accounts": [
{
"id": 2,
"account_number": "<account-number>",
"bank_id": 2,
"ifsc": "<ifsc>",
"swift_code": "<swift>",
"account_holder_name": "<holder-name>",
"iban": "<iban>",
"status": "active"
}
]
},
{ "id": 35, "bank_name": "HDFC Bank", "bank_code": "HDFC", "head_office": "Thiruvananthapuram", "…": "…" }
]
}GET /api/v1/logistics/list-delivery-methods
Purpose: active delivery methods (status = "Y") with translated names and their prices.
Screens: Delivery step (F4) of Finalize Order, the delivery chip in Restaurant, sales filters, Offline Data "Delivery methods".
Captured response (2 of 7 methods; translations shortened):
{
"status": "success",
"message": "List of delivery methods",
"data": [
{
"id": 147,
"name": "Toyou",
"code": "toyou",
"status": "Y",
"company_id": 2,
"translations": [
{ "locale": "ar", "key": "name", "value": "تويو" }
],
"prices": [
{ "id": 10, "delivery_method_id": 147, "price": "100.00" },
{ "id": 11, "delivery_method_id": 147, "price": "200.00" }
]
},
{
"id": 148,
"name": "Door Delivery",
"code": "door-delivery",
"status": "Y",
"translations": [
{ "locale": "ar", "key": "name", "value": "التوصيل إلى الباب" }
],
"prices": [
{ "id": 1, "delivery_method_id": 148, "price": "50.00" },
{ "id": 2, "delivery_method_id": 148, "price": "75.00" }
]
}
]
}The demo store shows seven methods: Toyou, Door Delivery, Store Takeaway, Car Delivery, Third Party logistics, Hungerstation and Dine In. A method with no prices shows as Free.
How the app reads the code
The app turns code (or the name) into upper case with underscores, then recognises these kinds:
Kind | Codes recognised | Extra field asked |
|---|---|---|
Store takeaway | STORETAKEAWAY, STORETAKEAWAY, TAKEAWAY, TAKEAWAY, PICKUP, STOREPICKUP, SELFPICKUP | none |
Car delivery | CARDELIVERY, CAR, CURBSIDE, CURBSIDEPICKUP, DRIVE_THROUGH | car number ( |
Door delivery | DOORDELIVERY, HOMEDELIVERY, DOOR, DELIVERY, STANDARD_DELIVERY | address (pincode too when ECOMMERCE_ENABLED is on) |
Third-party logistics | THIRDPARTYLOGISTICS, THIRD_PARTY, THIRDPARTY, 3PL, LOGISTICS | none |
Other | anything else (for example Toyou, Hungerstation, Dine In) | none |
So door-delivery becomes DOOR_DELIVERY (door delivery). The default method comes from company prop DEFAULT_DELIVERY_METHOD.
Delivery charge rule (POS)
The POS works out the charge itself:
- If FREE_DELIVERY_MINIMUM_AMOUNT is not enabled, the delivery charge is 0 for every method.
- If it is enabled with a value above 0 and the net total is at least that value, the charge is 0 (free delivery).
- Otherwise a manual override wins, if the cashier typed one.
- Otherwise the first price in the method's
priceslist (Door Delivery → 50.00, Toyou → 100.00).
The charge is sent as delivery_charge on add-to-order. When PRICE_ROUND_OFF is on, the net total is rounded before the delivery charge is added.
GET /api/v1/logistics/delivery-charge
Purpose: weight- and location-based delivery charge for a cart and address. The web storefront uses it; the POS uses the rule above instead.
Param | Rule |
|---|---|
| Required. |
| Required. |
| Required. |
When weight data, a location match or a rate is missing, the server falls back to a default charge of 5.00 with eta_days 3 and a note explaining the fallback. Missing address, store or cart returns "Required data not found".
GET /api/v1/discount/list-discounts
Purpose: coupons valid today for this store. The app downloads them so it can check a coupon while offline.
Param | Example | Effect |
|---|---|---|
|
| Coupons for this store plus company-wide coupons. |
When company setting DISCOUNT_AND_COUPON is off, the list is empty. Captured response:
{
"status": "success",
"message": "Discounts retrieved successfully",
"data": {
"data": [
{
"id": 19,
"coupon_code": "CLAUDETEST10",
"coupon_name": "CLAUDETEST10",
"store_id": 2,
"category_id": null,
"product_id": null,
"discount_type": "percent",
"discount_value": 10,
"valid_from_date": "2026-10-24",
"valid_to_date": "2026-10-31",
"discount_coupon_limit_count": 10,
"discount_coupon_limit_amount": "50.000",
"discount_coupon_min_amount": "50.000",
"discount_coupon_max_amount": null
},
{
"id": 20,
"coupon_code": "SPECIAL-OFFER",
"coupon_name": "SPECIAL OFFER",
"store_id": 2,
"discount_type": "fixed",
"discount_value": 20,
"valid_from_date": "2026-10-24",
"valid_to_date": "2026-10-31",
"discount_coupon_limit_count": 100,
"discount_coupon_limit_amount": "20.000",
"discount_coupon_min_amount": "100.000",
"discount_coupon_max_amount": "1000.000"
}
]
}
}Field | Meaning |
|---|---|
|
|
| Percent (max 100) or amount. |
| Cap on the discount amount. CLAUDETEST10 gives 10% but never more than 50.00. |
| Order total must be within these. |
| How many times the coupon can be used. |
| Limits the coupon to one product, or a category and its sub-categories. |
|
|
Coupon rules (app and server agree)
- The cart must have items.
- A store coupon only works in its store.
- Today must be within the validity dates.
- The order total must be within min and max amounts (message: "Coupon minimum order amount is 100.000.").
- The usage limit must not be reached.
- Only eligible lines count: lines that already have an offer are excluded; a product coupon counts only that product; otherwise a category coupon counts that category and its sub-categories. Product selection beats category.
- Percent: eligible total × value / 100. Fixed: the value. Then the cap (
limit_amount) applies, and the discount never exceeds the eligible total or the cart total.
Example with SPECIAL-OFFER on Layla's 658.00 order: 658.00 is between 100 and 1000, fixed 20, cap 20 → discount 20.00, total 638.00.
GET /api/v1/discount/apply-coupon
Purpose: server check of a coupon code. Used by Apply in the Discount & Coupon panel when online. (POST is also accepted.)
Param | Example | Rule |
|---|---|---|
|
| Required. |
|
| Order total. Required without |
| Required without | |
| Optional. | |
| Server cart, instead of price/product. | |
|
| The app sends the active store. |
Success:
{
"status": "success",
"message": "Coupon applied successfully",
"data": {
"data": {
"original_price": "658.00",
"discounted_price": "638.00",
"discount_amount": "20.00",
"coupon_code": "SPECIAL-OFFER",
"coupon_id": 20
}
}
}Failures use the error envelope with the reason (for example "Discounts and coupons are disabled", "Cart not found", or the coupon rule that failed). The order then carries coupon_id / coupon_code.
GET /api/v1/offers/pos-sync
Purpose: downloads product and category offers (automatic promotions) for the store, including future scheduled ones, so offer prices work offline. Called only when company prop POS_OFFERS is on.
Param | Example | Rule |
|---|---|---|
|
| Required; must belong to the company (else |
|
| Optional. Only changes since the last sync. The server may still send a full snapshot. |
Response shape:
{
"success": true,
"server_time": "2026-10-27T12:00:00+00:00",
"synced_at": "2026-10-27T15:00:00+03:00",
"company_timezone": "Asia/Riyadh",
"full_snapshot": true,
"selection": [
"product_before_category",
"store_specific_first",
"stock_specific_first",
"nearest_category_first",
"newest_line_first"
],
"offers": [
{
"id": 5,
"name": "Bucket Week",
"store_id": 2,
"target_type": "products",
"category_id": null,
"valid_from": "2026-10-27T00:00:00+03:00",
"valid_until": "2026-11-01T00:00:00+03:00",
"enabled": true,
"source_types": ["executive"],
"base_unit_only": true,
"version": 1,
"lines": [
{
"line_id": 9,
"product_id": 12445,
"stock_id": null,
"type": "percentage",
"value": 10,
"min_margin_price": 0,
"min_margin_percentage": 0
}
]
}
],
"removed_offer_ids": []
}The offer object above is an illustration of the shape; the demo had no active offers. When POS offers are off, offers is empty and full_snapshot is true.
Field | Meaning |
|---|---|
|
|
|
|
| Offer limited to one stock batch, or |
| The product's max-discount floor; an offer never takes the price below it, or below zero. |
| Offers apply to base-unit lines only (not pack/box sale units). |
| Offers to delete from the device cache. |
Offer precedence
Exactly one offer applies to a line; offers never stack. The winner is chosen in this order (the selection list):
- A product offer beats a category offer.
- A store-specific offer beats a company-wide offer.
- A batch (stock) offer beats a product-wide offer.
- The nearest category in the product's category tree wins.
- Then the newest line.
Also: a percentage is capped at 100%; the wholesale price beats the offer when the line qualifies for wholesale; coupons skip lines that already have an offer. An offer line is sent on the order with offer_id, offer_version and discount_origin: "offer", and the printed price is kept (pricing_mode: completed_sale).