> ## Documentation Index
> Fetch the complete documentation index at: https://developer.klikit.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Fetch the menu tree (store-level or business-level)

> Returns a 4-level menu tree
(sections → categories → items → modifier groups → modifiers)
scoped to your credential's business. Two modes, selected by
whether you supply the store coordinates:

### Store-level (`brand_id` + `branch_id` both present)

Returns the per-store snapshot served from the cached
`store_full_menus` blob. Reflects every per-store override
written via `zoneStores` plus the latest
[`publishMenu`](#operation/publishMenu). This is the same
payload klikit ships to aggregator marketplaces.

The envelope is `data.menu.branchInfo + data.menu.sections[]`.
Each section / category / item carries fields like
`isAvailable`, `stockEnabled`, `attachedWithStore` derived
from the store's overrides.

### Business-level (`brand_id` and `branch_id` both omitted)

Returns the business-level tree with no store overrides applied —
the raw shape sitting in the `sections / categories / items /
modifier_groups` tables before any store has populated.

partner-api assembles this on the fly by walking the four list
endpoints in parallel and joining them client-side, so the
payload is a superset of what those endpoints return:

* [`listSections`](#operation/listSections) for the top of the
  tree (each section carries `linkedCategories[]` ids).
* [`listCategories`](#operation/listCategories) supplies the
  category records.
* [`listItems`](#operation/listItems) supplies items (top-level
  + modifier options — modifier options are items with
  `onlyModifier: true`).
* [`listModifierGroups`](#operation/listModifierGroups)
  supplies the MG records.

The envelope is `data.menu.businessInfo + data.menu.sections[]`.
Store-specific fields (`isAvailable`, `attachedWithStore`, …)
are absent because there's no store context.

### Errors

* Passing **only one** of `brand_id` / `branch_id` returns
  `400 request_invalid`. Either both together (store mode) or
  neither (business mode).
* Store-mode 404 when `(brand_id, branch_id)` doesn't resolve
  to one of your business's stores.

Both modes use the same `menus:read` scope.




## OpenAPI

````yaml /partner-api/api-reference/openapi.yaml get /v1/partner/menus
openapi: 3.1.0
info:
  title: klikit Partner API
  version: 0.2.0
  description: >
    Public partner-facing API for POS / ERP integrations with klikit.


    Every response is wrapped in the canonical

    [Envelope](#tag/Concepts) which carries a `request_id` for support

    correlation and a machine-readable `error.code` on failure.


    All endpoints are authenticated via HTTP Basic Auth using credentials

    issued by your klikit operator contact. The plaintext `secret_key` is

    shown only once at issuance — store it securely; rotate to receive a

    new one if lost.


    Each credential is scoped to a single klikit business and grants a

    fixed set of permissions (e.g. `orders:read`, `stock:update`). The

    full scope vocabulary is documented under

    [Concepts](#tag/Concepts).


    ---


    ## Quickstart


    Pick the **integration mode** that matches what your system needs to

    do. Each mode lists the minimum endpoints to call, in order. Skip

    any mode you don't need — they're independent.


    ### 0. Bootstrap (everyone runs this once)


    1. Confirm auth + your business hierarchy:
       - `GET ` [`/v1/partner/brands`](#operation/listBrands) — returns
         every brand under your business plus the business metadata.
         Capture the `id` for each brand you plan to operate on.
       - `GET ` [`/v1/partner/branches/{id}`](#operation/getBranch) for
         each branch you'll target.
       - To self-serve provisioning, use
         [`createBrand`](#operation/createBrand) /
         [`updateBrand`](#operation/updateBrand) and
         [`createBranch`](#operation/createBranch) /
         [`updateBranch`](#operation/updateBranch). Scopes:
         `brands:create` / `brands:update` and
         `branches:create` / `branches:update`.
    2. Resolve provider ids (klikit, GrabFood, foodpanda, …) that you'll
       reference inside `VisibilitiesMap` and `PriceMap` payloads:
       - `GET ` [`/v1/partner/providers`](#operation/listProviders).
         The catalog is also pinned in
         [`ProviderId`](#/components/schemas/ProviderId) — ids are stable
         across environments, so partners typically cache them and don't
         re-fetch per request.
    3. Pull the static dictionaries you'll need to decode payloads:
       [`OrderStatus`](#/components/schemas/OrderStatus),
       [`PaymentStatus`](#/components/schemas/PaymentStatus),
       [`PaymentMethod`](#/components/schemas/PaymentMethod),
       [`PaymentChannel`](#/components/schemas/PaymentChannel).

    Required scopes for bootstrap: `brands:read`, `branches:read`.


    ---


    ### Mode A — Order operations (POS integration)


    For partners whose POS / OMS accepts orders coming **out** of klikit

    (klikit places, you fulfil + ack the lifecycle).


    | Step | Endpoint | Purpose |

    |---|---|---|

    | A1 | `GET ` [`/v1/partner/orders/statuses`](#operation/listOrderStatuses)
    | One-time reference fetch — pair with
    [`OrderStatus`](#/components/schemas/OrderStatus). |

    | A2 | `GET ` [`/v1/partner/orders`](#operation/listOrders) | Poll for new
    orders. Use `from` / `to` to bound by `created_at`. |

    | A3 | `GET ` [`/v1/partner/orders/{id}`](#operation/getOrder) | Fetch full
    order detail if your list call is summary-only. |

    | A4 | `PATCH ` [`/v1/partner/orders/status`](#operation/updateOrderStatus)
    | Move an order through `ACCEPTED → READY → DELIVERED`, or to `CANCELLED`
    with a reason. Body: `{ id, status, platform: "enterprise" }`. |

    | A5 | `PATCH `
    [`/v1/partner/orders/{id}/comment`](#operation/setOrderComment) | Attach an
    internal note to an order (visible inside the klikit operator console). |


    Scopes: `orders:read`, `orders:update`.


    ---


    ### Mode B — Read the live store menu


    For partners that want to render klikit's menu inside their own

    storefront / POS — no menu authoring on the partner side.


    1. `GET `
    [`/v1/partner/menus?brand_id={brandID}&branch_id={branchID}`](#operation/getMenu)
       — returns the published, store-specific tree with sections →
       categories → items → modifier groups → modifiers nested.

    That's it. The full payload shape lives under

    [`MenuTree`](#/components/schemas/MenuTree); item-level

    `prices`, `visibilities`, `enabled`, `isAvailable` and stock signals

    are all in-tree.


    Scopes: `menus:read`.


    ---


    ### Mode C — Push a menu (partner-authored)


    For partners that own the source-of-truth menu and want to seed /

    update klikit. Walk this on each brand:


    | Step | Endpoint | Notes |

    |---|---|---|

    | C1 | `POST ` [`/v1/partner/menus/sections`](#operation/createSection) |
    Top of the tree. Capture `data.id`. |

    | C2 | `POST ` [`/v1/partner/menus/categories`](#operation/createCategory) |
    Under sections. Capture `data.id`. |

    | C3 | `POST `
    [`/v1/partner/menus/categories/link`](#operation/linkCategoryToSection) |
    Attach the category id(s) to the section id from C1. |

    | C4 | `POST ` [`/v1/partner/menus/items`](#operation/createItem) | One per
    sellable item. `onlyModifier:false`. |

    | C5 | `POST `
    [`/v1/partner/menus/items/categories/link`](#operation/linkCategoriesToItem)
    | Attach the item to its category. |

    | C6 | `POST `
    [`/v1/partner/menus/modifier-groups`](#operation/createModifierGroup) | One
    per "Size", "Toppings", … choice header. |

    | C7 | `POST ` [`/v1/partner/menus/items`](#operation/createItem) | One per
    modifier option, this time with `onlyModifier:true`. |

    | C8 | `POST `
    [`/v1/partner/menus/modifier-groups/modifiers`](#operation/linkModifiersToGroup)
    | Attach modifier-option ids to the modifier-group id. |

    | C9 | `POST `
    [`/v1/partner/menus/modifier-groups/link`](#operation/linkModifierGroupToItem)
    | Attach the modifier group to the item(s). Min/max + modifier ids must be
    re-stated here. |

    | C10 | `POST ` [`/v1/partner/menus/sync`](#operation/syncMenuToStores) |
    Push the business-level definition into each store. |

    | C11 | `POST ` [`/v1/partner/menus/publish`](#operation/publishMenu) |
    Rebuild the cached store snapshot served by `getMenu`, and ship to provider
    marketplaces. |


    Updating an existing menu uses the same chain, with two gotchas

    spelled out under the [`menu-publish`](#tag/menu-publish) tag:

    the sync `fieldIncludeOnSync` escape hatch (so updated schedules /

    visibility actually overwrite store overrides) and the mandatory

    `publishMenu` cache refresh.


    **Stock & OOS** — same scope set; reach for these as needed:


    - [`snoozeItem`](#operation/snoozeItem) — take an item OOS for a
      window (the temporary "we just ran out" UX).
    - [`enableItems`](#operation/enableItems) — bulk enable / disable
      items indefinitely (the permanent OOS toggle). The same shape
      exists for sections
      ([`enableSections`](#operation/enableSections)) and categories
      ([`enableCategories`](#operation/enableCategories)).
    - [`updateItemStocks`](#operation/updateItemStocks) — per-branch
      stock counts in bulk.
    - [`listOOSActivitySummaries`](#operation/listOOSActivitySummaries)
      plus the per-entity audit reads
      ([sections](#operation/listOOSActivitySections),
      [categories](#operation/listOOSActivityCategories),
      [items](#operation/listOOSActivityItems)) — the OOS history feed.

    Scopes: `menus:read`, `menus:create`, `menus:update`,

    optional `menus:delete` if you also clean up.


    ---


    ### Cross-cutting essentials


    - **Authentication** — `Authorization: Basic
    base64(partner_key:secret_key)`.
      Treat the secret like any other production credential. Rotation
      gives you a new secret with no overlap window.
    - **Errors** — switch on `error.code` (see
      [`ErrorBody`](#/components/schemas/ErrorBody)), never the message
      text. `request_id` is the support-correlation key.
    - **Idempotency** — write endpoints accept an `Idempotency-Key`
      header. Same key + same body within 24 h replays the cached
      response; same key + different body returns
      `409 state_idempotency_conflict`.
    - **Scoping** — every credential is pinned to one business. The
      gateway rewrites `business_id` (and the `filterByBusiness` filter
      on order reads) to your credential's value, so you cannot peek at
      another tenant's data even if you supply a different id.
    - **Rate limits** — the default is 600 req/min per credential. Hit
      the cap and the response is `429 rate_limit_exceeded`. Spread
      bulk writes accordingly.
  contact:
    name: klikit Partner API
    url: https://github.com/klikit/klikit-partner
servers:
  - url: https://api.dev.shadowchef.co
    description: Development gateway
security:
  - partnerBasicAuth: []
tags:
  - name: brands
    description: Read access to brands belonging to the partner's business.
  - name: branches
    description: Read access to branches.
  - name: providers
    description: |
      Catalog of order/menu providers (klikit, klikber, grab, foodpanda, …).
      Returned IDs are used as keys inside `VisibilitiesMap` and `PriceMap`
      when creating menu entities.
  - name: price-groups
    description: |
      Pricing groups (price tiers — "Mall", "Airport", …) let the same
      item sell at different prices per branch tier. Group ids are used
      as `price_group_id` on branch create/update and as the outer keys
      of `groupPrices` on item price writes. See the three-step recipe
      under [`listPriceGroups`](#operation/listPriceGroups).
  - name: orders
    description: Order lifecycle endpoints called by the partner.
  - name: menus
    description: Menu read endpoints — full tree fetch and structural reads.
  - name: menu-sections
    description: Sections live at the top of the menu tree.
  - name: menu-categories
    description: Categories sit under sections and contain items.
  - name: menu-items
    description: >-
      Items + per-item price / stock / availability / visibility / blackout /
      linking.
  - name: menu-modifier-groups
    description: >-
      Modifier groups, modifier-group ↔ item links, presets, and modifier-group
      ↔ modifier links.
  - name: menu-publish
    description: >
      Publish, populate, sync, and reset operations between the business

      menu and store overrides.


      ### How a business-level change reaches the partner-facing

      ### `getMenu` read


      The menu has three layers — business definition, per-store

      override, cached store snapshot — and partners interact with all

      three. After editing the business layer you need an explicit push

      to refresh each downstream layer:


      | Step                                       |
      Purpose                                                              |

      |--------------------------------------------|----------------------------------------------------------------------|

      | `PATCH /menus/sections/{id}` (or similar)  | Edit the business-level
      row.                                         |

      | `POST /menus/sync`                         | Replay business state into
      the per-store overrides. By default sync **preserves** the store's
      existing `availableTimes`, `isEnabled`, `visibilities`, `blackoutDates` —
      list whichever you want to force-overwrite in `fieldIncludeOnSync`. |

      | `POST /menus/publish`                      | Rebuild the cached
      `store_full_menus` blob that `getMenu` reads, and ship to
      providers.                                       |


      Skipping the publish step is the most common reason

      [`getMenu`](#operation/getMenu) keeps returning the old payload

      after a sync — see [`publishMenu`](#operation/publishMenu).
  - name: menu-oos
    description: OOS (out-of-stock) activity track reads.
  - name: webhooks
    description: >
      ## What this is


      Klikit pushes events to a URL **you** host. Whenever something

      happens on klikit that you care about — a new order arrives, an

      order's status changes, a customer modifies their cart — klikit

      makes an HTTP POST to your endpoint with the details. You don't

      need to poll.


      ## The three events you can subscribe to


      | Event | When it fires |

      |---|---|

      | `klikit.order.created.v2`     | A new order has just landed for one of
      your branches. This is the trigger for "ring the bell at the kitchen". |

      | `klikit.order.status.updated` | An order's status moved forward (e.g.
      *accepted → preparing → ready → delivered*) or was cancelled. |

      | `klikit.order.cart.updated`   | A customer changed items / quantities
      **before** finalising. Use this if you preview baskets in real time. |


      All three events POST the same envelope:


      ```json

      {
        "brand_id":  123,
        "branch_id": 456,
        "orders":    [ /* one or more orders, same shape as GET /v1/partner/orders */ ]
      }

      ```


      ## Getting set up


      1. **Tell your klikit operator the URL.** Webhook URLs are
         registered out-of-band during onboarding — give your klikit
         contact one URL per `(brand, branch, event)` tuple. There is
         no self-service registration endpoint today.
      2. **Get your `webhook_secret_key`** from the same operator. Treat
         it like a password: never log it, never put it in git.
      3. **Stand up a receiver.** See the worked examples in
         [examples/webhook-receiver](https://github.com/klikit/partner-api/tree/develop/examples/webhook-receiver)
         (Node + Python, ~80 lines each) — copy whichever fits your
         stack.

      ## What klikit sends with every delivery


      | Header | Meaning |

      |---|---|

      | `x-klikit-signature`  | `hex(HMAC-SHA256(your webhook_secret_key, raw
      request body))` |

      | `x-klikit-event-id`   | Unique per delivery attempt. Use this for
      idempotency — see below. |

      | `x-klikit-event-type` | One of the three event names above. |

      | `Content-Type`        | Always `application/json`. |


      ## How to verify the signature


      The header `x-klikit-signature` is an **HMAC-SHA256** computed

      over the **raw bytes** of the request body, using your

      `webhook_secret_key` as the key, and encoded as **lowercase

      hex**. Reject any delivery whose signature you cannot

      reproduce — it means the request was tampered with, or you have

      the wrong secret.


      ```js

      // Node

      const crypto = require("crypto");

      const expected = crypto
        .createHmac("sha256", WEBHOOK_SECRET)
        .update(rawBody)         // the raw bytes — NOT the parsed JSON
        .digest("hex");
      const ok = crypto.timingSafeEqual(
        Buffer.from(req.headers["x-klikit-signature"]),
        Buffer.from(expected),
      );

      ```


      ```python

      # Python

      import hmac, hashlib

      expected = hmac.new(
          WEBHOOK_SECRET.encode(),
          raw_body,                # the raw bytes — NOT the parsed JSON
          hashlib.sha256,
      ).hexdigest()

      ok = hmac.compare_digest(request.headers["x-klikit-signature"], expected)

      ```


      ```bash

      # Quick check from the shell

      printf '%s' "$RAW_BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"

      ```


      > **Critical gotcha.** Compute the HMAC over the **raw bytes**

      > klikit sent. If you parse the JSON and re-serialise before

      > verifying, key reordering or whitespace differences will break

      > the signature.


      ## What klikit expects back


      - **HTTP 2xx within 10 seconds.** Body is ignored. JSON or
        empty are both fine.
      - **Anything else (3xx, 4xx, 5xx, timeout, connection error)** is
        treated as a failed delivery. Klikit will retry with
        exponential backoff. Failed deliveries are persisted in
        `hookit.webhook_logs` and your operator can ask support to
        replay them if you needed to recover from downtime.

      ## Two things you must build in


      **1. Be idempotent.** Because klikit retries on non-2xx, the same

      `x-klikit-event-id` *will* arrive twice in some failure modes.

      Store the IDs you've already processed (Redis `SETNX`, Postgres

      unique index, …) and silently return 2xx for repeats.


      **2. Acknowledge fast, do work async.** Reply 200 first, then

      push to your POS / kitchen display / database on a background

      worker. If your endpoint takes 11 seconds to push to a slow

      downstream and then returns 200, klikit has already counted it

      as a timeout and queued a retry — you will process every order

      twice.


      ## Troubleshooting


      | Symptom | Most likely cause |

      |---|---|

      | `401 invalid signature` on every request | Reading the parsed body
      instead of the raw body, or wrong `webhook_secret_key` |

      | Duplicate orders showing up downstream | Non-idempotent handler — same
      `x-klikit-event-id` processed twice after a retry |

      | Klikit reports deliveries failing | Endpoint returning non-2xx, taking
      >10s, or unreachable from klikit's egress |

      | `x-klikit-*` headers missing on your side | Reverse proxy stripping
      `x-*` headers — most common with nginx; allow explicitly |
  - name: Concepts
    description: |
      Shared shapes that aren't endpoints — request envelope, error codes,
      scope vocabulary.
  - name: Reference dictionaries
    description: |
      Static numeric enums that show up inside order / payment / branch
      payloads. Use these to decode order + payment fields without an
      extra endpoint, and to look up the `country_id` / `city_id` /
      `currency_id` triple your [`createBranch`](#operation/createBranch)
      call needs.

      Order & payment:
      [`OrderStatus`](#/components/schemas/OrderStatus),
      [`PaymentStatus`](#/components/schemas/PaymentStatus),
      [`PaymentMethod`](#/components/schemas/PaymentMethod),
      [`PaymentChannel`](#/components/schemas/PaymentChannel),
      [`ProviderId`](#/components/schemas/ProviderId).

      Geo & currency:
      [`CountryId`](#/components/schemas/CountryId) — 24 active country
      ids with ISO codes,
      [`CityId`](#/components/schemas/CityId) — 252 cities grouped by
      country (the partner-side source-of-truth for the
      `(country_id, city_id)` pair core's branch create validates
      against),
      [`CurrencyId`](#/components/schemas/CurrencyId) — currency ids
      paired with the ISO 4217 codes you already use inside
      [`PriceMap`](#/components/schemas/PriceMap).
paths:
  /v1/partner/menus:
    get:
      tags:
        - menus
      summary: Fetch the menu tree (store-level or business-level)
      description: |
        Returns a 4-level menu tree
        (sections → categories → items → modifier groups → modifiers)
        scoped to your credential's business. Two modes, selected by
        whether you supply the store coordinates:

        ### Store-level (`brand_id` + `branch_id` both present)

        Returns the per-store snapshot served from the cached
        `store_full_menus` blob. Reflects every per-store override
        written via `zoneStores` plus the latest
        [`publishMenu`](#operation/publishMenu). This is the same
        payload klikit ships to aggregator marketplaces.

        The envelope is `data.menu.branchInfo + data.menu.sections[]`.
        Each section / category / item carries fields like
        `isAvailable`, `stockEnabled`, `attachedWithStore` derived
        from the store's overrides.

        ### Business-level (`brand_id` and `branch_id` both omitted)

        Returns the business-level tree with no store overrides applied —
        the raw shape sitting in the `sections / categories / items /
        modifier_groups` tables before any store has populated.

        partner-api assembles this on the fly by walking the four list
        endpoints in parallel and joining them client-side, so the
        payload is a superset of what those endpoints return:

        * [`listSections`](#operation/listSections) for the top of the
          tree (each section carries `linkedCategories[]` ids).
        * [`listCategories`](#operation/listCategories) supplies the
          category records.
        * [`listItems`](#operation/listItems) supplies items (top-level
          + modifier options — modifier options are items with
          `onlyModifier: true`).
        * [`listModifierGroups`](#operation/listModifierGroups)
          supplies the MG records.

        The envelope is `data.menu.businessInfo + data.menu.sections[]`.
        Store-specific fields (`isAvailable`, `attachedWithStore`, …)
        are absent because there's no store context.

        ### Errors

        * Passing **only one** of `brand_id` / `branch_id` returns
          `400 request_invalid`. Either both together (store mode) or
          neither (business mode).
        * Store-mode 404 when `(brand_id, branch_id)` doesn't resolve
          to one of your business's stores.

        Both modes use the same `menus:read` scope.
      operationId: getMenu
      parameters:
        - in: query
          name: brand_id
          schema:
            type: integer
            minimum: 1
          description: |
            Brand id of the target store. Required when `branch_id` is
            also passed. Omit (along with `branch_id`) to read the
            business-level tree.
        - in: query
          name: branch_id
          schema:
            type: integer
            minimum: 1
          description: |
            Branch id of the target store. Required when `brand_id` is
            also passed. Omit (along with `brand_id`) to read the
            business-level tree.
        - in: query
          name: tz
          schema:
            type: string
            example: Asia/Singapore
          description: Optional IANA timezone used to compute availability flags.
      responses:
        '200':
          description: |
            Menu tree. The example below is one section / one category /
            one item sliced verbatim from a prod store-mode response
            (brand 5297, branch 1293). Business-mode responses share the
            same shape and key names but with a much larger tree (no
            trimming applied server-side); the full payload weighs in
            at several MB on real businesses, so partners should expect
            to stream-parse if pulling business mode.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MenuEnvelope'
              examples:
                store_menu:
                  $ref: '#/components/examples/StoreMenuExample'
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/DownstreamUnavailable'
components:
  schemas:
    MenuEnvelope:
      description: >
        The `data.menu` payload is a recursive tree:


        ```

        menu

        ├── branchInfo            store-level metadata (currency, language)

        └── sections[]
            └── categories[]
                └── items[]
                    └── groups[]          modifier GROUPS (e.g. "Size")
                        └── modifiers[]   modifier OPTIONS (e.g. "Large")
                            └── groups[]  nested groups, for stacked modifiers
        ```


        On the wire the modifier-group key is `groups`; modifier options

        are under `modifiers`. A modifier (option) carries the same

        shape as a regular item — `id`, `title`, `price`, `visibilities`,

        `image`, plus its own `groups` array, so combos like

        pizza → size → toppings nest naturally. Map your SKUs to the

        `id` you see at each level.


        Most fields are language maps keyed by locale code (`{"en": "...", "id":
        "..."}`)

        so the same payload serves multiple storefronts.
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                menu:
                  $ref: '#/components/schemas/MenuTree'
          example:
            request_id: req_4d1b7e3f-1f06-4d3c-a14f-9c0b9f8a3e2c
            data:
              menu:
                branchInfo:
                  businessID: 42
                  brandID: 7
                  branchID: 19
                  countryID: 360
                  currencyID: 88
                  languageCode: en
                  currencyCode: IDR
                  currencySymbol: Rp
                sections:
                  - id: 1001
                    sequence: 1
                    title:
                      en: All Day Menu
                      id: Menu Sepanjang Hari
                    description:
                      en: Available 09:00 – 22:00
                    enabled: true
                    isAvailable: true
                    categories:
                      - id: 2010
                        sequence: 1
                        title:
                          en: Pizzas
                          id: Pizza
                        enabled: true
                        items:
                          - id: 30100
                            sequence: 1
                            title:
                              en: Margherita
                              id: Margherita
                            description:
                              en: Tomato, mozzarella, basil
                            price: 85000
                            enabled: true
                            isAvailable: true
                            image: https://cdn.klikit.io/items/30100.jpg
                            groups:
                              - id: 4001
                                title:
                                  en: Size
                                  id: Ukuran
                                minSelection: 1
                                maxSelection: 1
                                modifiers:
                                  - id: 50001
                                    title:
                                      en: Regular (9")
                                    price: 0
                                  - id: 50002
                                    title:
                                      en: Large (12")
                                    price: 25000
                              - id: 4002
                                title:
                                  en: Extra toppings
                                  id: Topping tambahan
                                minSelection: 0
                                maxSelection: 5
                                modifiers:
                                  - id: 50101
                                    title:
                                      en: Extra cheese
                                    price: 10000
                                  - id: 50102
                                    title:
                                      en: Pepperoni
                                    price: 12000
                                    groups:
                                      - id: 4101
                                        title:
                                          en: Spice level
                                        minSelection: 1
                                        maxSelection: 1
                                        modifiers:
                                          - id: 50201
                                            title:
                                              en: Mild
                                            price: 0
                                          - id: 50202
                                            title:
                                              en: Hot
                                            price: 0
    Envelope:
      type: object
      description: |
        Canonical response wrapper. Every response — success or error —
        carries the `request_id` so you can quote one id to klikit
        support to correlate a request end-to-end.
      properties:
        request_id:
          type: string
          example: req_4d1b7e3f-...
        data:
          description: Endpoint-specific payload on success.
        error:
          $ref: '#/components/schemas/ErrorBody'
      required:
        - request_id
    MenuTree:
      type: object
      description: Root of the menu payload.
      properties:
        branchInfo:
          $ref: '#/components/schemas/MenuBranchInfo'
        sections:
          type: array
          items:
            $ref: '#/components/schemas/MenuSection'
    ErrorBody:
      type: object
      description: >
        Machine-readable error code + human message. The code is stable

        across releases — switch on `code` in your client code rather

        than parsing the message text.


        Common codes you will encounter as a partner:


        | Code                            | HTTP |
        Meaning                                           |

        |---------------------------------|------|---------------------------------------------------|

        | `auth_missing`                  | 401  | Authorization header absent /
        malformed           |

        | `auth_invalid_credential`       | 401  | partner_key or secret_key did
        not verify          |

        | `auth_revoked`                  | 403  | Credential is
        revoked                             |

        | `auth_forbidden`                | 403  | Credential not authorized for
        the requested scope |

        | `request_invalid`               | 400  | Body / query parameters
        failed validation         |

        | `request_missing_idempotency_key` | 400 | Write endpoint called
        without `Idempotency-Key`   |

        | `request_invalid_range`         | 400  | Date range > 90
        days                              |

        | `resource_not_found`            | 404  | Order / store / mapping does
        not exist            |

        | `resource_unmapped`             | 404  | Stock / availability call
        referenced an unknown SKU |

        | `state_invalid_transition`      | 409  | Order PATCH not allowed by
        current state          |

        | `state_idempotency_conflict`    | 409  | Same Idempotency-Key reused
        with a different body |

        | `rate_limit_exceeded`           | 429  | Per-credential rate cap
        hit                       |

        | `downstream_unavailable`        | 502  | An internal klikit dependency
        is unreachable      |
      properties:
        code:
          type: string
          example: auth_invalid_credential
        message:
          type: string
        details:
          description: Optional structured detail (validation errors, current state).
      required:
        - code
        - message
    MenuBranchInfo:
      type: object
      properties:
        businessID:
          type: integer
        brandID:
          type: integer
        branchID:
          type: integer
        countryID:
          type: integer
        currencyID:
          type: integer
        languageCode:
          type: string
          example: en
        currencyCode:
          type: string
          example: IDR
        currencySymbol:
          type: string
          example: Rp
        providerIDs:
          type: string
          description: Comma-separated aggregator provider ids active on the branch.
    MenuSection:
      type: object
      properties:
        id:
          type: integer
        sequence:
          type: integer
        title:
          $ref: '#/components/schemas/LangMap'
        description:
          $ref: '#/components/schemas/LangMap'
        image:
          type: string
          nullable: true
        enabled:
          type: boolean
        isAvailable:
          type: boolean
          description: Computed from section schedule + branch hours.
        categories:
          type: array
          items:
            $ref: '#/components/schemas/MenuCategory'
    LangMap:
      type: object
      description: |
        Locale-keyed text. Keys are ISO 639-1 codes (`en`, `id`, `ja`, ...).
        Use whichever key matches your storefront; klikit ships a value
        for every locale your business is configured for.
      additionalProperties:
        type: string
      example:
        en: Margherita
        id: Margherita
    MenuCategory:
      type: object
      properties:
        id:
          type: integer
        sequence:
          type: integer
        title:
          $ref: '#/components/schemas/LangMap'
        description:
          $ref: '#/components/schemas/LangMap'
        enabled:
          type: boolean
        items:
          type: array
          items:
            $ref: '#/components/schemas/MenuItem'
    MenuItem:
      type: object
      description: |
        Both top-level items and modifier options share this shape — a
        modifier option is just an item nested inside a group. The
        recursive `groups` array on this object is what enables
        stacked-modifier combos.
      properties:
        id:
          type: integer
        sequence:
          type: integer
        title:
          $ref: '#/components/schemas/LangMap'
        description:
          $ref: '#/components/schemas/LangMap'
        price:
          type: number
        image:
          type: string
          nullable: true
        enabled:
          type: boolean
        isAvailable:
          type: boolean
        stockQuantity:
          type: integer
          nullable: true
        groups:
          type: array
          items:
            $ref: '#/components/schemas/MenuModifierGroup'
    MenuModifierGroup:
      type: object
      properties:
        id:
          type: integer
        sequence:
          type: integer
        title:
          $ref: '#/components/schemas/LangMap'
        minSelection:
          type: integer
        maxSelection:
          type: integer
        modifiers:
          type: array
          items:
            $ref: '#/components/schemas/MenuItem'
  examples:
    StoreMenuExample:
      summary: >-
        One section / category / item slice of the published store menu (prod
        biz 398, brand 5297, branch 1293)
      value:
        request_id: 0af0f90815518ac157c838fd0e07ed5a
        data:
          menu:
            branchInfo:
              branchID: 1293
              brandID: 5297
              businessID: 398
              countryID: 4
              currencyCode: IDR
              currencyID: 4
              currencySymbol: ''
              languageCode: id
              providerIDs: 1,6,9,11,16,17
            sections:
              - id: 6825
                sequence: 1
                title:
                  en: Cimol Bojot AA Master Menu
                description: {}
                enabled: true
                image: ''
                isMealForOne: false
                visibilities:
                  '1': true
                availableTimes:
                  '0':
                    disabled: false
                    slots:
                      - startTime: 0
                        endTime: 2359
                  '1':
                    disabled: false
                    slots:
                      - startTime: 0
                        endTime: 2359
                  '2':
                    disabled: false
                    slots:
                      - startTime: 0
                        endTime: 2359
                  '3':
                    disabled: false
                    slots:
                      - startTime: 0
                        endTime: 2359
                  '4':
                    disabled: false
                    slots:
                      - startTime: 0
                        endTime: 2359
                  '5':
                    disabled: false
                    slots:
                      - startTime: 0
                        endTime: 2359
                  '6':
                    disabled: false
                    slots:
                      - startTime: 0
                        endTime: 2359
                blackoutDates: null
                categories:
                  - id: 39114
                    sequence: 1
                    title:
                      en: Cimol Bundling
                    description: {}
                    enabled: true
                    alcBeverages: false
                    blackoutDates: null
                    consentMessage: {}
                    isAgeRestricted: false
                    isGrabMealForOne: false
                    isHalal: false
                    isIdVerificationRequired: false
                    isMealForOne: false
                    visibilities:
                      '1': true
                    items:
                      - id: 289370
                        sequence: 1
                        title:
                          en: Cimol Bundling 1
                        description:
                          en: >-
                            Paket Bundling: - Cimol Bojot Porsi Kecil Isi ±100
                            Gram - Cimol Mozzarella Porsi Besar Isi 16 Pcs atau
                            Cimol Ayam/Beef Porsi Besar Isi 14 Pcs
                        enabled: true
                        itemIsEnabled: true
                        isGrabMealForOne: false
                        isHalal: false
                        maxQuantityPerDay: 1
                        preparationTime: 0
                        skuID: ''
                        stockEnabled: false
                        yieldCount: 1
                        vat: 0
                        allergenIDs: null
                        allergens: null
                        blackoutDates: null
                        groups: []
                        itemVisibilities:
                          '1': true
                        visibilities:
                          '1': true
                        oos:
                          available: true
                          snooze: null
                        prices:
                          '1':
                            IDR:
                              price: 25000
                              takeAwayPrice: 25000
                        resources: []
  responses:
    InvalidRequest:
      description: Request input is malformed or invalid.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        example: request_invalid
                      message:
                        type: string
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        example: auth_invalid_credential
                        enum:
                          - auth_missing
                          - auth_invalid_credential
    Forbidden:
      description: Authenticated but not allowed (revoked or out-of-scope).
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        enum:
                          - auth_revoked
                          - auth_forbidden
    NotFound:
      description: Resource does not exist (or is not visible to your credential).
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        example: resource_not_found
    DownstreamUnavailable:
      description: An internal klikit dependency is unreachable. Retry after a short delay.
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/Envelope'
              - type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                        example: downstream_unavailable
  securitySchemes:
    partnerBasicAuth:
      type: http
      scheme: basic
      description: |
        `Authorization: Basic base64(partner_key:secret_key)`.

        Credentials are issued by a klikit operator. The plaintext
        `secret_key` is shown once at issuance and cannot be retrieved
        later — store it securely. If lost, ask your operator to rotate
        the secret to receive a new one. The old secret stops working
        immediately on rotation; there is no overlap window.

````