Flavours Partner API

Changelog

Version history for the Flavours API — new endpoints, request/response changes, deprecations, and breaking changes.

This page tracks every change to the Flavours API that affects how you integrate with it: new endpoints, added or renamed request fields, changed responses, new query parameters, deprecations, and breaking changes. Entries are listed newest first.

How versions work

We follow Semantic Versioning: MAJOR.MINOR.PATCH.

  • PATCH (e.g. 2.1.0 → 2.1.1) — bug fixes, no contract changes. Safe to upgrade.
  • MINOR (e.g. 2.1.0 → 2.2.0) — new, backward-compatible features. Safe to upgrade.
  • MAJOR (e.g. 2.x → 3.0.0) — may include breaking changes. Review before upgrading.

Breaking changes are also called out inline with a ⚠ Breaking marker.

How to select a version

The API serves the contract that matches the X-API-Version request header (default 2.0.0). Every change below applies only to clients that send that version or higher — a client pinned to an older version keeps the old behavior, so you can upgrade field-by-field on your own schedule.

Integrations documentation — 2026-10-20

No new API version · documentation only, applies at every X-API-Version

Added

  • Integrations guide group: overview, quickstart, scopes, token lifecycle, errors, and security best practices for connecting a third-party system with OAuth client credentials (POST /oauth/token, one-hour access tokens).
  • No business endpoint accepts an integration access token yet. Each endpoint that does will be announced here and documented in the Integrations section.

Payment devices and spec sync — 2026-10-11

No new API version · backward compatible, applies at every X-API-Version

Added

  • Payment-device endpoints (FP-1127, FlavoursPOS #1076). Manage the payment terminals of a branch:

    EndpointPurpose
    GET /api/v2/payment-devicesList devices (pagination and filtering).
    POST /api/v2/payment-devicesCreate a device.
    GET /api/v2/payment-devices/{deviceId}Show one device.
    POST /api/v2/payment-devices/{deviceId}Update a device.
    DELETE /api/v2/payment-devices/{deviceId}Delete a device.
  • payment_method_keys: string[] on every payment-device response. The list is sorted; [] means the device fires for every payment method except CASH.

Behavior

  • On create and update, a missing payment_method_keys leaves the list unchanged; an explicit [] clears it.
  • CASH is rejected with 422.
  • Every key must be a payment method that is adopted and active for the market, otherwise 422.
  • Un-adopting a payment method removes its key from every device list.

Also in this spec refresh

The OpenAPI spec is mirrored from FlavoursPOS development, which also carries the following changes that were not in the previous docs revision. None of them needs a new X-API-Version.

  • Shift summary on attendances (FP-1083 phase 2, FlavoursPOS #1074). GET /api/v2/attendances, POST /api/v2/attendances and GET /api/v2/attendances/{attendanceId}: on X-API-Version >= 2.6.0 the shift summary is opt-in via includes[]=report (below 2.6.0 it is always attached). A closed shift answers with a frozen summary (report.frozen_at, report.payment_methods_snapshot, report.payment_methods[], cash_variance); any filter[entirely], filter[statuses] or filter[transaction][statuses] turns it into a live, never-stored figure with payment_methods_snapshot = false. The report object returned by POST /api/v2/attendances gains id, discount, payment, total, tax, cost, profit and the order_*/window_* epochs.
  • QR round on a money-locked order (FlavoursPOS #1074, building on the FP-1098 money lock in #1061). POST /api/v2/orders: a QR round on a table whose open order ZATCA has already reported answers 423 order.money_locked.
  • ⚠ Type change orders_* totals are now integers. GET /api/v2/reports/attendances/{attendanceId}/totals: orders_tax, orders_discount, orders_subtotal, orders_cost, orders_profit and the other orders_* counts and totals (all except orders_product_count, still a string) change from nullable string to integer. Clients that parse them as strings must be updated (FlavoursPOS #1074).

Workday reports and dashboard — 2026-10-10

No new API version · backward compatible, applies at every X-API-Version

Only branches with the workday switch ON change

Everything below is inert until the workday feature is enabled for the environment and a branch's owner turns its workday switch on. A branch whose switch is OFF, and every client that does not send the new parameters, gets exactly the same response as before. No client needs to change its X-API-Version.

Added

  • Workday range on the reports. These endpoints accept two optional query parameters next to their existing epoch window:

    ParameterTypeDescription
    filter[workday_min]string (Y-m-d)First workday of the range, inclusive.
    filter[workday_max]string (Y-m-d)Last workday, inclusive, at most 366 dates after workday_min.
    EndpointEpoch window still sent with them
    GET /api/v2/reports/overview and its sub-reports (/branches, /categories, /products, /payments, /tables, /cashiers, /customers, /drivers)filter[created_at_min], filter[created_at_max]
    GET /api/v2/reports/{totals,categories,cashiers,payments,tables}filter[created_at_after], filter[created_at_before]
    GET /api/v2/reports/products, /products/addon-groups, /products/{productId}/stats, /products/{productId}/ordersfilter[created_at_after], filter[created_at_before]
    GET /api/v2/reports/returns, /reports/returns/exportfilter[created_at_min], filter[created_at_max]
    GET /api/v2/ordersfilter[created_at_min], filter[created_at_max] (or created_at_after/before)

    Send both sets. A branch whose workday is ON is then cut by its workday (an order rung up after midnight or in overtime belongs to the workday it was taken in; a refund counts on the workday it was processed); OFF branches keep the epoch window. The epochs become required when the workday dates are sent. A reversed range, a range over 366 dates or a market you cannot access is 422.

  • meta.workday on the range reports. When the workday parameters are sent and the feature is on, the overview (index and /branches), the product report endpoints, the returns list and the order list add a block to the response meta: one entry per branch with its timezone, mode (WORKDAY or CALENDAR), the period start_at/close_at (epoch ms), workdays, the branch's own orders and total, and, for ranges of 31 dates or fewer, days[] with one row per date (source: ORDERS, ATTENDANCE, NONE or CALENDAR).

      "meta": {
        "code": 200,
        "status": "success",
        "message": "Returns report retrieved successfully.",
    +   "workday": {
    +     "branches": [
    +       {
    +         "branch_id": "0184d96e-4f2f-479a-aa5c-0cf1ab279013",
    +         "timezone": "Asia/Riyadh",
    +         "mode": "WORKDAY",
    +         "start_at": 1777873800000,
    +         "close_at": null,
    +         "workdays": 2,
    +         "orders": 1,
    +         "total": "40",
    +         "days": [
    +           {"date": "2026-05-04", "start_at": 1777873800000, "close_at": 1777924800000, "source": "ORDERS", "orders": 1, "total": "40"},
    +           {"date": "2026-05-05", "start_at": null, "close_at": null, "source": "NONE", "orders": 0, "total": "0"}
    +         ]
    +       }
    +     ]
    +   }
      }
  • workdays on the subscriber dashboard. GET /api/v2/dashboard/subscriber adds a workdays array (in both the >= 2.2.0 and the legacy shape) when at least one branch in scope has its workday ON: per branch the mode, the workday date shown as "Today", its state (NOT_STARTED, EARLY, OPEN, OVERTIME, CLOSED), the workday times (checkin_at, open_at, close_at, end_at, next_opening_at, epoch ms) and live / past windows with the branch's count and total. past.until is the like-for-like cut: while today's workday is running, the comparison counts the previous workday only up to the same elapsed point.

      "data": {
        "interval": {"past": {"min": 1791118200000, "max": 1791158400000}, "live": {"min": 1791204600000, "max": 1791244800000}},
        "stats": {"…": "unchanged keys"},
    +   "workdays": [
    +     {
    +       "branch_id": "72a1b30e-b89f-43d3-b7f6-58a0fe5294ca",
    +       "timezone": "Asia/Riyadh",
    +       "mode": "WORKDAY",
    +       "date": "2026-10-05",
    +       "state": "OPEN",
    +       "checkin_at": 1791204600000, "open_at": 1791205200000, "close_at": 1791241200000, "end_at": 1791244800000,
    +       "next_opening_at": 1791291600000,
    +       "live": {"workday_min": "2026-10-05", "workday_max": "2026-10-05", "start_at": 1791204600000, "close_at": 1791244800000, "count": 2, "total": 200},
    +       "past": {"workday_min": "2026-10-04", "workday_max": "2026-10-04", "start_at": 1791118200000, "close_at": 1791158400000, "until": 1791151200000}
    +     }
    +   ]
      }

Changed

  • Subscriber dashboard "today" (ON branches). The request is unchanged; the server now picks each ON branch's current workday (the one running now, overtime included, so an overnight branch at 01:00 still shows the workday that started yesterday; before opening, today's not-started workday with zeros). past is the previous workday (DAILY) or the previous period of workday dates. The DAILY chart buckets each branch by its local hour. interval becomes the envelope of the branches' windows.
  • V1 subscriber report. GET /api/summary/subscriber-report (and its PDF) with filter[period_type]=LAST_DAY reports the running workday on ON branches. The other V1 /summary/* endpoints are unchanged.
  • Daily order number (ON branches). number on orders created online (POST /api/v2/orders, the guest table QR and the waiter table open) counts per workday: an overnight workday keeps counting after midnight, and the next workday starts again at 0001. Offline-synced orders keep the number the app sent.
  • Kitchen, waiter and floor reports (ON branches). Parameters and payloads are unchanged; the business day of filter[date_from]/filter[date_to]/filter[week] is the branch's workday window, kitchen tickets and waiter orders count by their order's workday, and OFF branches use 04:00 in their own timezone instead of a fixed +03:00.
  • Kitchen display and device pairing (ON branches). The window of GET /api/v2/kds/tickets, the unrouted list, the cutover board and a pairing code's expires_at follow the branch's running workday instead of the next 04:00. Shapes are unchanged.

2.5.0 — 2026-08-02

Minor release · contains breaking changes

Add-on groups are not in the 2026-08-02 build

2.5.0 was published on 2026-08-02 with the legacy-import removal below. The add-on-group changes in this entry share the same 2.5.0 cutoff but ship later, so a client that sets X-API-Version: 2.5.0 against the current build gets the 410 Gone import behaviour and not the add-on-group payloads. Check the status page before you rely on them.

⚠ Breaking change

Add-ons can now be owned by a reusable add-on group instead of by a single product. Clients adopting 2.5.0 see the real owner on every add-on they read (modifierable_type: "PRODUCT_MODIFIER_ADDON_GROUP"), get per-group selection limits enforced when they place an order, and lose the legacy Excel product-import endpoints (410 Gone). Clients pinned below 2.5.0 keep receiving every add-on rewritten to look product-owned, so their payloads stay byte-identical to what they received before add-on groups existed.

Added

  • Add-on groups — a reusable library of add-ons you can attach to many products. A group carries name_en, name_ar, min_additionals, max_additionals and order, plus its own options; attaching it to a product makes those options part of that product's add-ons. Thirteen new endpoints:

    MethodPathDescription
    GET/api/v2/products/addon-groupsList the business's add-on groups.
    POST/api/v2/products/addon-groupsCreate a group.
    GET/api/v2/products/addon-groups/{groupId}Group detail.
    POST/api/v2/products/addon-groups/{groupId}Update a group.
    DELETE/api/v2/products/addon-groups/{groupId}Delete a group.
    POST/api/v2/products/addon-groups/{groupId}/optionsAdd an option to a group.
    GET/api/v2/products/addon-groups/{groupId}/options/{optionId}Option detail.
    POST/api/v2/products/addon-groups/{groupId}/options/{optionId}Update an option.
    DELETE/api/v2/products/addon-groups/{groupId}/options/{optionId}Delete an option.
    GET/api/v2/products/{productId}/addon-groupsList the groups attached to a product.
    POST/api/v2/products/{productId}/addon-groups/sortReorder a product's attached groups.
    POST/api/v2/products/{productId}/addon-groups/{groupId}Attach a group to a product.
    DELETE/api/v2/products/{productId}/addon-groups/{groupId}Detach a group from a product.

    Deleting a group that is still attached to any product is refused with 409 Conflict — detach it everywhere first.

  • Add-on option — price_mode and price_value. An option owned by a group can be priced FIXED (the ordinary stored price, and what omitting price_mode means), FREE (always zero), or PERCENT (price_value percent of the selected variant's pre-tax price, resolved server-side at order time and snapshotted onto the order, so editing the percentage later never reprices a past order). Both fields are hidden from clients below 2.5.0.

      {
        "id": 718168250238047,
        "name_en": "Garlic Sauce",
        "type": "ADDITIONAL",
        "price": 0,
        "total": 0,
    +   "price_mode": "PERCENT",
    +   "price_value": 10
      }

    Only the group option endpoints accept the pair. FREE and PERCENT options store zero price, tax and total. PERCENT additionally requires type: "ADDITIONAL" and forces is_tax_included to false (sending true is rejected), and its price_value must be greater than 0 and at most 100. On create, PERCENT requires price_value. On update, changing price_mode discards the stored amount, so the new mode's value must come with the switch — an update that leaves price_mode alone keeps the amount it already had.

    The two amount fields are mutually exclusive, and the wrong one is prohibited, not ignored — this is what an existing integration trips on first, because it keeps sending the price it always sent:

    Effective price_modepriceprice_value
    FIXED — also what omitting price_mode meansallowed; required on create when type is "ADDITIONAL"prohibited
    FREEprohibitedprohibited
    PERCENTprohibitedallowed; required on create

    Sending a prohibited field is a 422, even when its value is the one already stored — so {"price_mode": "FREE", "price": 2} fails; drop the price. The prohibitions are identical on create and update. "Effective" matters on update: when you omit price_mode, the rules are evaluated against the mode the option already has, not against FIXED, so an update that only edits the name of a FREE option must still not carry a price. is_tax_included is also required on option create (it is optional on update).

  • Add-on group object — options_count and is_min_unsatisfiable. options_count is the number of options the same reader would be served for that group; is_min_unsatisfiable is true when the group's min_additionals cannot actually be satisfied, so a client can render the group without deadlocking the cart. Both are hidden below 2.5.0, and is_min_unsatisfiable is only present on the reads that carry options_count.

      {
        "id": "0d556865-cfd0-4eeb-991f-1c44133dc68c",
        "name_en": "Sauces",
        "min_additionals": 2,
        "max_additionals": 3,
    +   "options_count": 1,
    +   "is_min_unsatisfiable": true
      }

    Two things make a minimum unsatisfiable, and the flag does not distinguish them: the group has fewer options than the minimum (as above — a minimum of two on a group with one option), or the group's own max_additionals is lower than its minimum. The second looks impossible, because the group create and update endpoints cross-validate the pair and 422. It is still reachable: a product write copies the product's min_additionals / max_additionals onto its default group without that cross-check, and the product endpoints only compare the two values when both are present in the same request body — so sending min_additionals on its own can leave a stored maximum below it. Send both keys together on a product write if you want them to stay consistent.

  • addonGroups includes on the product endpoints. addonGroups, addonGroups.options, addonGroups.options.ingredients.ingredient, addonGroups.options.recipes.recipe and addonGroups.options.prices.paymentMethod are accepted in includes[] on the product list and detail endpoints, returning each product's attached groups as their own objects. Available at every version.

  • addon_group timestamps on GET /api/v2/sync/timestamps — an additive key alongside the existing ones, so a client can tell whether the add-on-group library changed since its last sync. Available at every version.

Changed

  • ⚠ Breaking Add-ons owned by a group now report that ownership in the flat additionals / additionalModifiers lists on the product list and detail endpoints. A 2.5.0 client sees the group morph; below 2.5.0 the same row is rewritten to look product-owned. The modifier id is identical on both sides.

      {
        "id": 718168252720184,
        "name_en": "Garlic Sauce",
    -   "modifierable_type": "PRODUCT_MODIFIER_PRODUCT",
    -   "modifierable_id": "9f1c…-product-uuid",
    -   "product_id": "9f1c…-product-uuid"
    +   "modifierable_type": "PRODUCT_MODIFIER_ADDON_GROUP",
    +   "modifierable_id": "0d556865-…-group-uuid",
    +   "product_id": null
      }

    product_id is null for options created through the add-on-group option endpoints. Add-ons written through a product's flat additionals[] keep that product's id even after they land in its default group, so do not infer product_id from modifierable_type.

  • ⚠ Breaking The same rewrite applies to the add-on rows echoed back by the order calculate and create responses (POST /api/v2/orders/calc, POST /api/v2/orders). Below 2.5.0 a group-owned add-on comes back as PRODUCT_MODIFIER_PRODUCT with modifierable_id and product_id set to the ordered product; at 2.5.0+ it keeps its group morph. Only the response is affected — what you send is unchanged.

  • ⚠ Breaking Per-group selection limits are enforced at order time on calculate, create and update. For a 2.5.0+ client, each attached group's min_additionals / max_additionals is checked against the add-ons selected from that group, and a violation is rejected with 400 naming the group (Choose at least 2 from Sauces. / You can choose up to 3 from Sauces.). This changes an outcome, not just a payload shape: an order body that succeeded below 2.5.0 can be refused at 2.5.0.

    The per-group check is an addition, not a replacement. The product-level min_additionals / max_additionals rule — one count across all the add-ons on the line — is still enforced at every version, 2.5.0 and above included, and still rejects with 400. Adopting 2.5.0 gives you two gates where you had one, so a line has to satisfy both. The product-level rule runs first, so a line that breaks both reports the product-level message (You can only add 3 add-ons.), not the group one. In practice the two agree: a product's own columns are kept in step with its default group in both directions. They come apart on a product assembled from shared library groups, which normally has no default group and so leaves its own columns null — there the per-group rule is the only one with anything to say.

    On a group, send max_additionals: null for no maximum — that is the only value the API accepts for it. 0 also reads as no maximum, but you cannot write it: the group create and update endpoints validate max_additionals as min:1 and answer 422. A group is only ever 0 because a product whose own max_additionals is 0 synced onto its default group, and on the product the column keeps its original meaning of zero add-ons allowed. A group's minimum is clamped to what is actually selectable, so a group whose min_additionals exceeds its option count cannot deadlock an order — the same condition is_min_unsatisfiable reports.

  • The flat additionals list is derived, not gated. Every version keeps receiving one flat list of a product's add-ons, whichever way each row is owned: group-owned rows are merged in alongside product-owned ones, product-owned first. A product whose add-ons were moved into a group therefore returns the same additionals payload as before the move for any client below 2.5.0.

  • Sending a flat additionals[] on product create/update still works, at every version. Those rows are reconciled into the product's default add-on group, created on demand, and echoing an existing add-on id back updates that row instead of duplicating it. Omitting the key changes nothing; sending it empty ([] or null) clears the product's flat add-ons. Groups attached from the shared library are never touched by a flat write. min_additionals / max_additionals stay in step between the product and its default group in both directions.

  • stock_modifiers_sum_stock is now always an integer, never null — at every version. The product object's add-on stock total is a SUM() over the product's add-on rows, and it used to be handed back raw: a product with no add-on rows summed to SQL NULL and the field came back null. It is now read through an accessor that coerces the sum to an integer and adds the group-owned rows to it, so a product with no add-ons returns 0 instead of null. The value for a product that has add-on rows is unchanged.

      {
        "id": "7c12ecd9-6644-43ef-b883-8dd1230eb261",
        "name_en": "Flat White",
    -   "stock_modifiers_sum_stock": null
    +   "stock_modifiers_sum_stock": 0
      }

    This one is not version-gated. It is a consequence of add-ons being able to live in a group — the total has to span both owners — and it applies to every client, including those pinned below 2.5.0. If you branch on stock_modifiers_sum_stock === null to mean "no add-ons", switch that test to === 0 before this ships. The API reference reflects the new type: the field is integer on every endpoint that returns it, where four of those schemas previously showed string / nullable: true.

Removed

  • ⚠ Breaking The legacy Excel product-import endpoints are gone for 2.5.0+ clients. POST /api/v2/import/products, POST /api/v2/import/products/full, GET /api/v2/import/products/template and GET /api/v2/import/products/full/template now answer 410 Gone instead of 426 Upgrade Required. The error body names the replacements under import_url, template_url, upload_url and status_url — use the bulk import endpoints (POST /api/v2/bulk/products/import, GET /api/v2/bulk/products/import/template, POST /api/v2/bulk/products/import/upload-url) and poll GET /api/v2/bulk-jobs.

    Those six keys are nested under data, in the standard { meta, data } error envelope every endpoint uses — not at the top level. Read data.import_url, not import_url.

    - HTTP/1.1 426 Upgrade Required
    + HTTP/1.1 410 Gone
      {
        "meta": {
    -     "code":    426,
    +     "code":    410,
          "status":  "error",
          "message": "This import method has been replaced. Use the import centre instead."
        },
        "data": {
          "import_url":   "/v2/bulk/products/import",
          "template_url": "/v2/bulk/products/import/template",
          "upload_url":   "/v2/bulk/products/import/upload-url",
          "status_url":   "/v2/bulk-jobs",
    -     "removed":      false,
    +     "removed":      true,
          "removed_in":   "2.5.0"
        }
      }

2.4.0 — 2026-07-26

Minor release · no breaking changes

Added

  • Payment-method config object — target, method, scope and sort are now returned. All four were hidden from clients below 2.4.0. They appear on the payment-method config reads (GET /api/v2/payment-methods/settings, GET /api/v2/payment-methods/settings/{configId}), on the config writes (POST/DELETE /api/v2/payment-methods/{methodKey}/config), and wherever else that object is serialized.

      {
        "id": 12,
        "payment_method_key": "VISA",
        "mode": "ENTIRELY",
        "is_active": true,
    +   "target": "ORDER",
    +   "method": "AFTER_TAX",
    +   "scope": "PROPORTIONAL",
    +   "sort": 1
      }
    • target — ORDER or PRODUCT: whether the config applies to the order total or to individual products.
    • method — AFTER_TAX, EXCLUSIVE or INCLUSIVE: how the fee sits relative to tax.
    • scope — SINGLE, ALWAYS or PROPORTIONAL: how the fee is applied across payments.
    • sort — display order.

    method and scope are omitted when target is "PRODUCT", at 2.4.0 and above — they do not apply to a product-targeted config. Do not treat their absence as a downgrade to an older contract; check target first.


2.3.0 — 2026-06-29

Minor release · contains breaking changes

⚠ Breaking change

Several response fields were renamed and logo_img on payment-method creation now expects a hosted URL instead of an uploaded file. These changes apply only to clients sending X-API-Version: 2.3.0 or higher — clients pinned to an earlier version keep the old field names and upload behavior, so existing integrations are unaffected until they opt in.

Changed

  • Category reference renamed product_category_id → category_id on every response carrying a category reference: the product object, order line items, product/category sort entries, kitchen categories, and sector categories.

      {
        "id": "…",
    -   "product_category_id": 12,
    +   "category_id": 12
      }
  • Product object — preparation_time → duration.

      {
        "id": "…",
        "name": "Flat White",
    -   "preparation_time": 5
    +   "duration": 5
      }
  • Modifier reference renamed product_modifier_id → modifier_id on the product price and product ingredient objects.

      {
        "id": "…",
    -   "product_modifier_id": 34,
    +   "modifier_id": 34
      }
  • Kitchen reference renamed kitchen_group_id → kitchen_id on the kitchen category and printer-kitchen objects.

      {
        "id": "…",
    -   "kitchen_group_id": 7,
    +   "kitchen_id": 7
      }
  • ⚠ Breaking POST /api/v2/payment-methods — logo_img now expects a URL string instead of a file upload. Upload the logo to your storage first, then send its URL. Clients below 2.3.0 keep uploading the image file as multipart form data.

    - // multipart/form-data
    - logo_img: <binary image file>            // required|image|max:1024
    + // application/json
    + "logo_img": "https://cdn.example.com/logos/apple-pay.png"   // required|string|url
  • ⚠ Breaking market_id and branch_id are no longer returned on the product ingredient and product price objects (GET /api/v2/products/{productId}/ingredients, GET /api/v2/products/{productId}/prices, and anywhere either object is embedded). Clients below 2.3.0 keep receiving both fields.


2.2.3 — 2026-07-22

Patch release · contains breaking changes

⚠ Breaking change

Despite the PATCH version number, 2.2.3 changes the shape of the product price object. Clients pinned below 2.2.3 keep the old flat fields.

Changed

  • ⚠ Breaking Product price object — the flat payment-method fields were replaced by a polymorphic reference. A product price is no longer tied to a payment method by a payment_method_key column with its own money fields; it now points at whatever it prices via priceable_type + priceable_id, carrying a single value and value_type. priceable_id holds a registry key — a payment-method key when priceable_type is "PAYMENT_METHOD", an order-type key when it is "ORDER_TYPE". Affects the product price and product-modifier price objects — GET /api/v2/products/{productId}/prices and its {priceId} detail — including where they are pulled in via includes[] on the product endpoints. Applies to clients sending X-API-Version: 2.2.3 or higher.

      {
        "id": 10,
        "product_id": "7c12ecd9-6644-43ef-b883-8dd1230eb261",
    -   "payment_method_key": "VISA",
    -   "price": "20",
    -   "tax": "0",
    -   "total": "20",
    -   "is_tax_included": true
    +   "priceable_type": "PAYMENT_METHOD",
    +   "priceable_id": "VISA",
    +   "value": 20,
    +   "value_type": "NOMINAL"
      }

    value_type is NOMINAL or PERCENT. Writes were not gated: sending the legacy payment_method_key on a product price still works at every version and is mapped to priceable_type: "PAYMENT_METHOD" server-side, so only your response parsing needs to change when you adopt 2.2.3.

    This cutoff is independent of the 2.3.0 rename on the same object (product_modifier_id → modifier_id, plus market_id / branch_id) — the product price object carries both cutoffs.


2.2.1 — 2026-07-05

Patch release · contains breaking changes

⚠ Breaking change

Despite the PATCH version number, 2.2.1 removes nine fields from the printer object and replaces them with one nested object. Clients pinned below 2.2.1 keep the flat flags.

Changed

  • ⚠ Breaking Printer object — the flat boolean flags were replaced by a settings object. The nine legacy is_* flags are no longer returned to 2.2.1+ clients; the same values now live under settings, keyed by setting name, on GET /api/v2/printers and its {printerId} detail. Below 2.2.1 the flat flags are returned and settings is withheld, so the two shapes never appear together. Applies to clients sending X-API-Version: 2.2.1 or higher.

      {
        "id": 1,
        "name": "Front Counter Printer",
    -   "is_multi_lang":     false,
    -   "is_show_product":   false,
    -   "is_open_drawer":    false,
    -   "is_postpaid_print": false,
    -   "is_img_print":      false,
    -   "is_auto_print":     false,
    -   "is_app_to_app":     false,
    -   "is_raster":         true,
    -   "is_pdf":            false
    +   "settings": {
    +     "MULTILINGUAL":   false,
    +     "PRODUCT_SHOW":   false,
    +     "DRAWER_OPEN":    false,
    +     "PRINT_POSTPAID": false,
    +     "PRINT_IMAGE":    false,
    +     "PRINT_AUTO":     false,
    +     "APP_TO_APP":     false,
    +     "HAS_SPACE":      false,
    +     "RASTER":         true,
    +     "PDF":            false
    +   }
      }

    When present, settings always carries all ten keys — any key missing from storage is normalised to false. A printer with nothing stored returns "settings": null, so guard for null before indexing into it.

    One flat flag currently leaks — do not depend on it. add_space is at present still returned to 2.2.1+ clients alongside settings, mirroring settings.HAS_SPACE. That is a defect rather than a contract: the withholding list is built from the setting names, and the name it produces for this one (has_space) is not the name the printer object actually exposes (add_space), so the flag is missed. Expect it to disappear once that is corrected. Read settings.HAS_SPACE at 2.2.1+ and treat add_space as removed along with the other nine flat flags.


2.2.0 — 2026-06-15

Minor release · contains breaking changes

⚠ Breaking change

The dashboard and report endpoints changed their response shape: flat, prefixed keys were replaced by nested objects. Clients that read these responses must update their parsing when adopting 2.2.0. Clients pinned below 2.2.0 continue to receive the flat structure.

Changed

  • Dashboard endpoints (GET /api/v2/dashboards, GET /api/v2/dashboard/{admin,merchant,subscriber}) — flat keys consolidated into nested objects. interval_* keys become a nested interval object, and the payload sections were renamed to chart, stats, orders, products, ingredients.

      {
    -   "interval_past_start_at": 1717200000000,
    -   "interval_past_end_at":   1717286400000,
    -   "interval_live_start_at": 1718064000000,
    -   "interval_live_end_at":   1718150400000,
    -   "chart_revenues":     [ … ],
    -   "overview_totals":    { … },
    -   "totals_by_status":   { … },
    -   "totals_by_type":     { … },
    -   "recent_orders":      [ … ],
    -   "featured_products":  [ … ],
    -   "out_of_stock_items": [ … ]
    +   "interval": {
    +     "past": { "min": 1717200000000, "max": 1717286400000 },
    +     "live": { "min": 1718064000000, "max": 1718150400000 }
    +   },
    +   "chart":       [ … ],
    +   "stats":       { … },
    +   "orders":      [ … ],
    +   "products":    [ … ],
    +   "ingredients": [ … ]
      }
  • Order statistics (the stats block of the dashboard) — flat prev_* / curr_* / percent_* keys replaced by nested past, live, and percent objects, each keyed by count, total, tax, cost, cost_unit, profit. The per-type and per-status breakdowns moved under types and statuses, and their inner keys dropped the _total suffix (e.g. overall_total → overall, success_total → success).

      {
    -   "prev_total_orders": 120,
    -   "prev_total_sales":  "4500.00",
    -   "curr_total_orders": 150,
    -   "curr_total_sales":  "5200.00",
    -   "percent_total_orders": 25.0
    +   "past":    { "count": 120, "total": "4500.00", "tax": "…", "cost": "…", "cost_unit": "…", "profit": "…" },
    +   "live":    { "count": 150, "total": "5200.00", "tax": "…", "cost": "…", "cost_unit": "…", "profit": "…" },
    +   "percent": { "count": 25.0, "total": 15.6, "tax": "…", "cost": "…", "cost_unit": "…", "profit": "…" }
      }
  • GET /api/v2/reports/overview — response restructured. The created_at_min / created_at_max keys move into a nested interval object (min / max), monetary values are now returned as raw numeric strings instead of money()-formatted strings, the discount and subtotal keys were removed, a new cost_unit key was added (item cost before tax), and cost now means item cost including tax.

      {
    -   "created_at_min": 1717200000000,
    -   "created_at_max": 1718150400000,
        "count": 150,
    -   "discount": "120.00",
    -   "subtotal": "5000.00",
    -   "payment":  "30.00",
    -   "profit":   "3800.00",
    -   "total":    "5200.00",
    -   "cost":     "1200.00",
    -   "tax":      "200.00"
    +   "interval": { "min": 1717200000000, "max": 1718150400000 },
    +   "payment":   "30.00",
    +   "profit":    "3800.00",
    +   "total":     "5200.00",
    +   "cost_unit": "1200.00",
    +   "cost":      "1400.00",
    +   "tax":       "200.00"
      }
  • Attendance report — the flat total_* summary keys were replaced by the full report object, which now exposes payment_stats and product_stats as arrays. Legacy keys such as total_order, total_revenue, total_payment_fees, total_tax, total_discount, total_cost_item, total_cost, total_profit, total_dining_order, total_dining_revenue, total_take_away_order, total_take_away_revenue, order_start_at, and order_close_at are no longer returned for 2.2.0+ clients.

Fixed

  • Product ingredient — ingredient_id now returns the real ingredient id. Before 2.2.0 this field was always 1; it now reflects the actual related ingredient and is typed as a string.

      {
        "id": "…",
    -   "ingredient_id": 1
    +   "ingredient_id": "9f1c…-uuid"
      }

2.1.0 — 2026-04-30

Minor release · contains breaking changes

⚠ Breaking change

Time and billing fields on several objects were renamed, and the Renewal endpoints now require X-API-Version: 2.1.0 or higher. Clients pinned below 2.1.0 keep the old field names; the Renewal endpoints reject them with 426 Upgrade Required.

Added

  • Ingredient object — cost is now returned. It was hidden for clients below 2.1.0.

      {
        "id": "…",
        "name": "Espresso Beans",
    +   "cost": "0.85"
      }

Changed

  • Attendance object — closing-time fields renamed:

      {
    -   "end_at": 1717286400000,
    -   "end_cash": "150.00",
    -   "end_schedule_at": 1717286400000
    +   "close_at": 1717286400000,
    +   "close_cash": "150.00",
    +   "close_schedule_at": 1717286400000
      }
  • Market setting object — balance renamed to credit (now a raw decimal rather than a money()-formatted string), the subscription keys were renamed, and country_code is no longer returned (use the related country instead):

      {
    -   "balance": "1,200.00",
    -   "subscription_key": "PRO",
    -   "plan_option_key": "MONTHLY",
    -   "subscription_option_key": "MONTHLY",
    -   "country_code": "SA"
    +   "credit": "1200.0000",
    +   "plan_key": "PRO",
    +   "cycle_key": "MONTHLY"
      }
  • Renewal branch object — end_at renamed to close_at, and the same subscription-key renames as the market setting object (subscription_key → plan_key, plan_option_key / subscription_option_key → cycle_key).

  • Plan reconciliation queries (branch, branch-setting, and market plan data) — the aggregated end_at column was renamed to close_at in the response rows.

  • Order tax object — taxable_id changed type from integer to string.

      {
    -   "taxable_id": 42
    +   "taxable_id": "42"
      }

Removed

  • ⚠ Breaking Renewal endpoints no longer support clients below 2.1.0. POST/PUT create, update, custom, preview, and calculate routes under the Renewal API now return 426 Upgrade Required for X-API-Version < 2.1.0.

2.0.0 — Initial release

Baseline of the Flavours v2 API. Every entry above describes changes relative to this version. Requests that omit the X-API-Version header are served the 2.0.0 contract.

On this page