> ## 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.

# Upsert a klikit-side comment on an order



## OpenAPI

````yaml /partner-api/api-reference/openapi.yaml patch /v1/partner/orders/{id}/comment
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/orders/{id}/comment:
    patch:
      tags:
        - orders
      summary: Upsert a klikit-side comment on an order
      operationId: setOrderComment
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      requestBody:
        $ref: '#/components/requestBodies/PassthroughBody'
      responses:
        '200':
          $ref: '#/components/responses/EnvelopeOK'
components:
  requestBodies:
    PassthroughBody:
      description: |
        Passthrough body forwarded verbatim to the underlying klikit
        service. `business_id` is overwritten to your credential's
        business so partners cannot escape scope; everything else
        passes through unchanged. Refer to klikit's internal service
        documentation for the exact field set per endpoint.
      required: true
      content:
        application/json:
          schema:
            type: object
            additionalProperties: true
  responses:
    EnvelopeOK:
      description: Success — payload wrapped in the canonical Envelope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Envelope'
  schemas:
    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
    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
  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.

````