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:
Endpoint Purpose 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 exceptCASH.
Behavior
- On create and update, a missing
payment_method_keysleaves the list unchanged; an explicit[]clears it. CASHis rejected with422.- 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/attendancesandGET /api/v2/attendances/{attendanceId}: onX-API-Version >= 2.6.0the shift summary is opt-in viaincludes[]=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); anyfilter[entirely],filter[statuses]orfilter[transaction][statuses]turns it into a live, never-stored figure withpayment_methods_snapshot = false. Thereportobject returned byPOST /api/v2/attendancesgainsid,discount,payment,total,tax,cost,profitand theorder_*/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 answers423 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_profitand the otherorders_*counts and totals (all exceptorders_product_count, still a string) change from nullablestringtointeger. 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:
Parameter Type Description 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.Endpoint Epoch window still sent with them GET /api/v2/reports/overviewand 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](orcreated_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.workdayon 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 responsemeta: one entry per branch with itstimezone,mode(WORKDAYorCALENDAR), the periodstart_at/close_at(epoch ms),workdays, the branch's ownordersandtotal, and, for ranges of 31 dates or fewer,days[]with one row per date (source:ORDERS,ATTENDANCE,NONEorCALENDAR)."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"} + ] + } + ] + } } -
workdayson the subscriber dashboard.GET /api/v2/dashboard/subscriberadds aworkdaysarray (in both the>= 2.2.0and the legacy shape) when at least one branch in scope has its workday ON: per branch themode, the workdaydateshown as "Today", itsstate(NOT_STARTED,EARLY,OPEN,OVERTIME,CLOSED), the workday times (checkin_at,open_at,close_at,end_at,next_opening_at, epoch ms) andlive/pastwindows with the branch'scountandtotal.past.untilis 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).
pastis the previous workday (DAILY) or the previous period of workday dates. TheDAILYchart buckets each branch by its local hour.intervalbecomes the envelope of the branches' windows. - V1 subscriber report.
GET /api/summary/subscriber-report(and its PDF) withfilter[period_type]=LAST_DAYreports the running workday on ON branches. The other V1/summary/*endpoints are unchanged. - Daily order number (ON branches).
numberon 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 at0001. 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
windowofGET /api/v2/kds/tickets, the unrouted list, the cutover board and a pairing code'sexpires_atfollow 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_additionalsandorder, plus its own options; attaching it to a product makes those options part of that product's add-ons. Thirteen new endpoints:Method Path Description 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_modeandprice_value. An option owned by a group can be pricedFIXED(the ordinary stored price, and what omittingprice_modemeans),FREE(always zero), orPERCENT(price_valuepercent 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 below2.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.
FREEandPERCENToptions store zeroprice,taxandtotal.PERCENTadditionally requirestype: "ADDITIONAL"and forcesis_tax_includedtofalse(sendingtrueis rejected), and itsprice_valuemust be greater than0and at most100. On create,PERCENTrequiresprice_value. On update, changingprice_modediscards the stored amount, so the new mode's value must come with the switch — an update that leavesprice_modealone 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
priceit always sent:Effective price_modepriceprice_valueFIXED— also what omittingprice_modemeansallowed; required on create when typeis"ADDITIONAL"prohibited FREEprohibited prohibited PERCENTprohibited allowed; 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 theprice. The prohibitions are identical on create and update. "Effective" matters on update: when you omitprice_mode, the rules are evaluated against the mode the option already has, not againstFIXED, so an update that only edits the name of aFREEoption must still not carry aprice.is_tax_includedis also required on option create (it is optional on update). -
Add-on group object —
options_countandis_min_unsatisfiable.options_countis the number of options the same reader would be served for that group;is_min_unsatisfiableistruewhen the group'smin_additionalscannot actually be satisfied, so a client can render the group without deadlocking the cart. Both are hidden below2.5.0, andis_min_unsatisfiableis only present on the reads that carryoptions_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_additionalsis lower than its minimum. The second looks impossible, because the group create and update endpoints cross-validate the pair and422. It is still reachable: a product write copies the product'smin_additionals/max_additionalsonto 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 sendingmin_additionalson its own can leave a stored maximum below it. Send both keys together on a product write if you want them to stay consistent. -
addonGroupsincludes on the product endpoints.addonGroups,addonGroups.options,addonGroups.options.ingredients.ingredient,addonGroups.options.recipes.recipeandaddonGroups.options.prices.paymentMethodare accepted inincludes[]on the product list and detail endpoints, returning each product's attached groups as their own objects. Available at every version. -
addon_grouptimestamps onGET /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/additionalModifierslists on the product list and detail endpoints. A2.5.0client sees the group morph; below2.5.0the same row is rewritten to look product-owned. The modifieridis 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_idisnullfor options created through the add-on-group option endpoints. Add-ons written through a product's flatadditionals[]keep that product's id even after they land in its default group, so do not inferproduct_idfrommodifierable_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). Below2.5.0a group-owned add-on comes back asPRODUCT_MODIFIER_PRODUCTwithmodifierable_idandproduct_idset to the ordered product; at2.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'smin_additionals/max_additionalsis checked against the add-ons selected from that group, and a violation is rejected with400naming 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 below2.5.0can be refused at2.5.0.The per-group check is an addition, not a replacement. The product-level
min_additionals/max_additionalsrule — one count across all the add-ons on the line — is still enforced at every version,2.5.0and above included, and still rejects with400. Adopting2.5.0gives 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 columnsnull— there the per-group rule is the only one with anything to say.On a group, send
max_additionals: nullfor no maximum — that is the only value the API accepts for it.0also reads as no maximum, but you cannot write it: the group create and update endpoints validatemax_additionalsasmin:1and answer422. A group is only ever0because a product whose ownmax_additionalsis0synced 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 whosemin_additionalsexceeds its option count cannot deadlock an order — the same conditionis_min_unsatisfiablereports. -
The flat
additionalslist 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 sameadditionalspayload as before the move for any client below2.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-onidback updates that row instead of duplicating it. Omitting the key changes nothing; sending it empty ([]ornull) clears the product's flat add-ons. Groups attached from the shared library are never touched by a flat write.min_additionals/max_additionalsstay in step between the product and its default group in both directions. -
stock_modifiers_sum_stockis now always an integer, nevernull— at every version. The product object's add-on stock total is aSUM()over the product's add-on rows, and it used to be handed back raw: a product with no add-on rows summed to SQLNULLand the field came backnull. 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 returns0instead ofnull. 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 onstock_modifiers_sum_stock === nullto mean "no add-ons", switch that test to=== 0before this ships. The API reference reflects the new type: the field isintegeron every endpoint that returns it, where four of those schemas previously showedstring/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/templateandGET /api/v2/import/products/full/templatenow answer410 Goneinstead of426 Upgrade Required. The error body names the replacements underimport_url,template_url,upload_urlandstatus_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 pollGET /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. Readdata.import_url, notimport_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,scopeandsortare now returned. All four were hidden from clients below2.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—ORDERorPRODUCT: whether the config applies to the order total or to individual products.method—AFTER_TAX,EXCLUSIVEorINCLUSIVE: how the fee sits relative to tax.scope—SINGLE,ALWAYSorPROPORTIONAL: how the fee is applied across payments.sort— display order.
methodandscopeare omitted whentargetis"PRODUCT", at2.4.0and above — they do not apply to a product-targeted config. Do not treat their absence as a downgrade to an older contract; checktargetfirst.
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_idon 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_idon the product price and product ingredient objects.{ "id": "…", - "product_modifier_id": 34, + "modifier_id": 34 } -
Kitchen reference renamed
kitchen_group_id→kitchen_idon the kitchen category and printer-kitchen objects.{ "id": "…", - "kitchen_group_id": 7, + "kitchen_id": 7 } -
⚠ Breaking
POST /api/v2/payment-methods—logo_imgnow expects a URL string instead of a file upload. Upload the logo to your storage first, then send its URL. Clients below2.3.0keep 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_idandbranch_idare 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 below2.3.0keep 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_keycolumn with its own money fields; it now points at whatever it prices viapriceable_type+priceable_id, carrying a singlevalueandvalue_type.priceable_idholds a registry key — a payment-method key whenpriceable_typeis"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}/pricesand its{priceId}detail — including where they are pulled in viaincludes[]on the product endpoints. Applies to clients sendingX-API-Version: 2.2.3or 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_typeisNOMINALorPERCENT. Writes were not gated: sending the legacypayment_method_keyon a product price still works at every version and is mapped topriceable_type: "PAYMENT_METHOD"server-side, so only your response parsing needs to change when you adopt2.2.3.This cutoff is independent of the
2.3.0rename on the same object (product_modifier_id→modifier_id, plusmarket_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
settingsobject. The nine legacyis_*flags are no longer returned to2.2.1+ clients; the same values now live undersettings, keyed by setting name, onGET /api/v2/printersand its{printerId}detail. Below2.2.1the flat flags are returned andsettingsis withheld, so the two shapes never appear together. Applies to clients sendingX-API-Version: 2.2.1or 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,
settingsalways carries all ten keys — any key missing from storage is normalised tofalse. 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_spaceis at present still returned to2.2.1+ clients alongsidesettings, mirroringsettings.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. Readsettings.HAS_SPACEat2.2.1+ and treatadd_spaceas 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 nestedintervalobject, and the payload sections were renamed tochart,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
statsblock of the dashboard) — flatprev_*/curr_*/percent_*keys replaced by nestedpast,live, andpercentobjects, each keyed bycount,total,tax,cost,cost_unit,profit. The per-type and per-status breakdowns moved undertypesandstatuses, and their inner keys dropped the_totalsuffix (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. Thecreated_at_min/created_at_maxkeys move into a nestedintervalobject (min/max), monetary values are now returned as raw numeric strings instead ofmoney()-formatted strings, thediscountandsubtotalkeys were removed, a newcost_unitkey was added (item cost before tax), andcostnow 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 exposespayment_statsandproduct_statsas arrays. Legacy keys such astotal_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, andorder_close_atare no longer returned for2.2.0+ clients.
Fixed
-
Product ingredient —
ingredient_idnow returns the real ingredient id. Before2.2.0this field was always1; 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 —
costis now returned. It was hidden for clients below2.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 —
balancerenamed tocredit(now a raw decimal rather than amoney()-formatted string), the subscription keys were renamed, andcountry_codeis 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_atrenamed toclose_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_atcolumn was renamed toclose_atin the response rows. -
Order tax object —
taxable_idchanged type from integer to string.{ - "taxable_id": 42 + "taxable_id": "42" }
Removed
- ⚠ Breaking Renewal endpoints no longer
support clients below
2.1.0.POST/PUTcreate, update, custom, preview, and calculate routes under the Renewal API now return426 Upgrade RequiredforX-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.