openapi: 3.1.0
info:
  title: Dukkan Platform API
  version: 1.0.0
  description: "PII-free app API. Money is integer minor units scaled by each Money object's own
    `decimals` field (Dukkan deviates from ISO 4217: SYP is zero-decimal — never scale with an ISO
    table). List endpoints use opaque cursors, every write requires Idempotency-Key, and tokens are
    budgeted at 180 requests/minute (60 writes/minute) — a 429 carries Retry-After.
    `components.schemas` and the `x-dukkan-*` extensions are generated from the zod contracts in
    shared/types/platform-api/v1 (`yarn gen:platform-openapi`); paths, parameters and responses are
    hand-written."
servers:
  - url: https://dukkan.one/platform-api/v1
security:
  - bearerAuth: []
tags:
  - name: Installation
    description: Identity of the token's install and store
  - name: Store
    description: Public identity and checkout facts of the token's store
  - name: Orders
  - name: Products
  - name: Discounts
  - name: Inventory
  - name: Webhooks
paths:
  /installation:
    get:
      tags:
        - Installation
      operationId: getInstallation
      summary: Identify the install and store behind this token
      description: Needs no scope. Returns the platform-issued ids an app must key its storage on
        (`install.id`, `store.id`), the effective scope set every request is checked against, the
        store's currency exponent, locale and timezone, and the current webhook subscription without
        its secret. `store.slug` is display-only and can change.
      responses:
        '200':
          description: The install and its store
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/Installation'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /store:
    get:
      tags:
        - Store
      operationId: getStore
      summary: Public identity and checkout facts of the store
      description: Needs no scope; everything here is already public on the storefront. Carries the
        pricing currency, the currencies a customer may pay in, the store-adjusted FX table checkout
        applies (integer `scaled / 10^scale`, base at identity), the enabled manual payment methods
        with the ShamCash id shown at checkout, the province vocabulary for
        `shipping_address.province`, and the public https origin (a connected custom domain wins).
      responses:
        '200':
          description: The store profile
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/Store'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /orders:
    get:
      tags:
        - Orders
      operationId: listOrders
      x-dukkan-scope: orders:read
      summary: List orders
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/OrderStatus'
        - name: created_at_min
          in: query
          schema:
            type: string
            format: date-time
        - name: updated_at_min
          in: query
          description: Incremental sync for status/payment changes (cursor stays keyed on created_at)
          schema:
            type: string
            format: date-time
        - name: tag
          in: query
          description: Only orders carrying this tag (case-insensitive)
          schema:
            type: string
            maxLength: 40
      responses:
        '200':
          description: Cursor page of PII-free orders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      tags:
        - Orders
      operationId: createOrder
      x-dukkan-scope: orders:create
      summary: Create an order with the prices the merchant agreed
      description: 'Lines carry the agreed unit price (catalog variants or custom lines with no variant);
        totals are recomputed server-side (subtotal = Σ quantity × unit_price_minor, total =
        subtotal − discount + shipping, tax 0) and every side effect of a storefront order follows:
        stock decrement obeying policy (409 `insufficient_stock` with `details.lines[]` and nothing
        written), CRM link by phone, merchant push and email, `order.created` with `source: app`,
        and a timeline row naming the app. Specific codes: 422 `currency_not_allowed`, 404
        `variant_not_found`, 409 `variant_unavailable`, 409 `payment_method_disabled`. The response
        echoes the customer details the app supplied regardless of `clients:read`.'
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: Order created (or the recorded response on an idempotent replay)
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/OrderDetail'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/RateLimited'
  /orders/{id}:
    get:
      tags:
        - Orders
      operationId: getOrder
      x-dukkan-scope: orders:read
      summary: Get an order and immutable line snapshots
      description: '`customer` and `shipping_address` are present ONLY when the install holds the
        DPA-gated `clients:read` scope; without it the keys are absent, not null.'
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: Order detail
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/OrderDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
    patch:
      tags:
        - Orders
      operationId: updateOrder
      x-dukkan-scope: orders:write
      summary: Update the note, add or remove tags, or merge the app's own attributes
      description: Money, lines, customer and address are immutable after creation. Tags are set
        operations (add/remove) so the merchant's and other apps' tags survive. `attributes` is the
        calling app's private namespace; `null` deletes a key; `reference.url` must be https on the
        app's registered origin.
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateOrderRequest'
      responses:
        '200':
          description: The updated order
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/OrderDetail'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/RateLimited'
  /orders/{id}/status:
    patch:
      tags:
        - Orders
      operationId: updateOrderStatus
      x-dukkan-scope: orders:write
      summary: Transition order status through the server matrix
      description: Money-bearing statuses (completed, partially_refunded, refunded) are reachable only
        through the refund writers. A transition the matrix does not allow answers 409 with
        `current_status` in the details.
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateOrderStatusRequest'
      responses:
        '200':
          description: Status transitioned
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
  /orders/{id}/fulfillments:
    post:
      tags:
        - Orders
      operationId: createFulfillment
      x-dukkan-scope: fulfillments:write
      summary: Fulfill selected line quantities; ship the order once every line is fulfilled
      description: Quantities above the still-unfulfilled amount of a line answer 409. Emits
        `fulfillment.created`, plus `fulfillment.requested` when the fulfillment is created as
        `pending` (a pickup waiting to be booked).
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateFulfillmentRequest'
      responses:
        '201':
          description: Fulfillment created; the order auto-ships only when every line is fulfilled AND the
            fulfillment was created as "shipped"
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
  /orders/{id}/fulfillments/{fulfillmentId}:
    patch:
      tags:
        - Orders
      operationId: updateFulfillment
      x-dukkan-scope: fulfillments:write
      summary: Update tracking, mark delivered, or cancel a fulfillment
      description: Identity and lines are immutable. Legal transitions are pending → shipped → delivered
        and pending|shipped → cancelled. Cancelling releases the quantity the fulfillment consumed,
        so the line can be fulfilled again. Terminal (delivered/cancelled) fulfillments reject
        further edits with 409.
      parameters:
        - $ref: '#/components/parameters/Id'
        - name: fulfillmentId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateFulfillmentRequest'
      responses:
        '200':
          description: 'Fulfillment updated. When the last outstanding fulfillment is delivered and every line
            is covered, the order transitions to "delivered" (order_status: "delivered" in the
            response); order_status_skipped is true when that transition was refused by the order''s
            current state. COD payment status is untouched — order.paid rides the payments ledger.'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
  /orders/{id}/refunds:
    post:
      tags:
        - Orders
      operationId: createRefund
      x-dukkan-scope: refunds:write
      summary: Append a refund against captured funds
      description: '`amount_minor` may not exceed paid minus already refunded. POS orders are refunded at
        the point of sale and answer 409 here. `restock` additionally requires the inventory:write
        scope.'
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRefundRequest'
      responses:
        '201':
          description: Refund created. Refundability is gated on remaining captured funds, not order status;
            when the status matrix has no refund edge (e.g. a terminal `cancelled`) the money still
            moves and `order_status_skipped` reports that the status was left untouched.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
  /webhooks:
    get:
      tags:
        - Webhooks
      operationId: getWebhookSubscription
      summary: Read this install's webhook subscription
      description: The signing secret is never returned; it can only be rotated.
      responses:
        '200':
          description: The active subscription, or null when none is configured
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    oneOf:
                      - type: 'null'
                      - $ref: '#/components/schemas/WebhookSubscription'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
    put:
      tags:
        - Webhooks
      operationId: upsertWebhookSubscription
      summary: Create or replace this install's webhook subscription
      description: 'One endpoint per install; a PUT fully replaces the topic list. The signing secret is
        returned EXACTLY ONCE — on creation, or when rotate_secret is true — and can never be read
        back afterwards. Topics are scope-gated: subscribing to a topic requires the read scope that
        governs its payload. endpoint_url must be a public HTTPS URL.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertWebhookSubscriptionRequest'
      responses:
        '200':
          description: Subscription created or replaced; `secret` present only when minted
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/WebhookSubscription'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
    delete:
      tags:
        - Webhooks
      operationId: deleteWebhookSubscription
      summary: Stop delivery for this install
      responses:
        '204':
          description: Subscription revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /webhooks/test:
    post:
      tags:
        - Webhooks
      operationId: createWebhookTestEvent
      summary: Fire one signed test delivery (sandbox stores only)
      description: "Writes a real outbox event for the topic, built from the sandbox store's own records
        (overridable through `data`), fans it out to this install's subscription and lets the
        delivery worker sign and send it with the same secret and SSRF checks as production traffic.
        The envelope carries `test: true`. Requires an active subscription that includes the topic
        and the scope that governs it. Budget: 30 per ten minutes per install. Answers 403
        `sandbox_required` on real stores, 409 `webhook_not_subscribed` /
        `webhook_topic_not_subscribed` / `sample_unavailable` when the rehearsal cannot be built."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookTestEventRequest'
      responses:
        '202':
          description: Event written and fanned out; the worker sends within about a minute
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/WebhookTestEvent'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          description: Override data exceeds 16 KB
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
  /products:
    get:
      tags:
        - Products
      operationId: listProducts
      x-dukkan-scope: products:read
      summary: List or search products and variants
      description: With `q` the page is ranked by similarity and carries no cursor (at most `limit`
        results); without it the cursor pages newest first. `available_quantity` is null unless the
        install also holds inventory:read.
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
        - name: updated_at_min
          in: query
          description: Incremental sync filter (the cursor itself keys on immutable created_at)
          schema:
            type: string
            format: date-time
        - name: q
          in: query
          description: Case-insensitive substring match on product name, variant name, SKU, DSIN and barcode
          schema:
            type: string
            minLength: 2
            maxLength: 80
        - name: status
          in: query
          schema:
            type: string
            enum:
              - active
              - out_of_stock
              - hidden
              - discontinued
              - no_variants
        - name: ids
          in: query
          description: Comma-separated product ids (at most 100)
          schema:
            type: string
      responses:
        '200':
          description: Product page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /products/{id}:
    get:
      tags:
        - Products
      operationId: getProduct
      x-dukkan-scope: products:read
      summary: Get one product with its variants
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: Product detail
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: '#/components/schemas/Product'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
  /products/variants:
    get:
      tags:
        - Products
      operationId: listVariants
      x-dukkan-scope: products:read
      summary: Batch variant lookup by id
      description: Re-validate quoted lines before sending an offer or creating an order. Unknown ids are
        omitted, never errors. `available_quantity` needs inventory:read.
      parameters:
        - name: ids
          in: query
          required: true
          description: Comma-separated variant ids (at most 100)
          schema:
            type: string
      responses:
        '200':
          description: The variants that exist in the store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VariantList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /discounts:
    get:
      tags:
        - Discounts
      operationId: listDiscounts
      x-dukkan-scope: discounts:read
      summary: List discounts (read-only)
      description: 'Requires the discounts:read scope. Discounts are read-only for apps: redemption
        accounting stays first-party, `used_count` is the app-visible redemption summary.'
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Discount page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscountPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
  /inventory/movements:
    get:
      tags:
        - Inventory
      operationId: listInventoryMovements
      x-dukkan-scope: inventory:read
      summary: List append-only inventory movements
      parameters:
        - $ref: '#/components/parameters/Cursor'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Movement page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MovementPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      tags:
        - Inventory
      operationId: createInventoryMovement
      x-dukkan-scope: inventory:write
      summary: Append an inventory movement; absolute stock writes are forbidden
      description: The response never includes the resulting absolute stock; read the movement list or the
        product for that.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInventoryMovementRequest'
      responses:
        '201':
          description: Movement appended
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
webhooks:
  storeEvent:
    post:
      operationId: receiveStoreEvent
      summary: Signed store event delivery (at-least-once)
      description: 'Delivered for every subscribed topic. Verify `X-Dukkan-Hmac-Sha256` (hex HMAC-SHA256
        of `{X-Dukkan-Timestamp}.{raw body}` with the signing secret of the install named in the
        signed `install_id`), reject timestamps older than five minutes, deduplicate on the signed
        envelope `id`, and refuse a `store_id` that differs from the install you hold. `sequence` is
        strictly increasing per store with gaps — order by it, never count on contiguity. Topics
        require the matching granted scope (see `x-dukkan-topic-scopes`); `app.uninstalled` needs no
        scope and still delivers after revocation. Respond 2xx with a small body within the timeout;
        anything else is retried with exponential backoff up to 8 attempts, after which the delivery
        is dead-lettered and sustained failures disable the subscription. Sandbox rehearsals fired
        through POST /webhooks/test carry `test: true`.'
      parameters:
        - name: X-Dukkan-Delivery-Id
          in: header
          required: true
          schema:
            type: string
            format: uuid
          description: Delivery attempt id (unsigned; retries reuse it)
        - name: X-Dukkan-Install-Id
          in: header
          required: true
          schema:
            type: string
            format: uuid
          description: Unsigned hint of the signed envelope install_id
          for secret lookup before verification: null
        - name: X-Dukkan-Event
          in: header
          required: true
          schema:
            $ref: '#/components/schemas/WebhookTopic'
        - name: X-Dukkan-Timestamp
          in: header
          required: true
          schema:
            type: string
            description: Unix seconds
        - name: X-Dukkan-Api-Version
          in: header
          required: true
          schema:
            type: string
            description: Dated v1 revision that produced this payload
        - name: X-Dukkan-Hmac-Sha256
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEnvelope'
      responses:
        '200':
          description: Acknowledged — any 2xx stops retries
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  parameters:
    Id:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    Cursor:
      name: cursor
      in: query
      schema:
        type: string
        maxLength: 512
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Unique per logical write, at most 200 characters, remembered for 24 hours. Replaying
        the same key with the same body returns the recorded response; the same key with a different
        body answers 409.
      schema:
        type: string
        minLength: 1
        maxLength: 200
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing, invalid, expired, or revoked token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Required live scope is missing
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found in the token-bound store
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnprocessableEntity:
      description: Well-formed body that fails the contract; `details.fields[]` names the paths
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: State transition, inventory, refund, or idempotency conflict
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Token budget exhausted (180 requests/minute, 60 writes/minute per install)
      headers:
        Retry-After:
          schema:
            type: integer
            description: Seconds until the budget resets
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: 'Stable machine code: invalid_request, unauthorized, forbidden, not_found, conflict,
                payload_too_large, rate_limited, internal_error, or a more specific code named on
                the operation.'
            message:
              type: string
            request_id:
              description: Echo of X-Request-Id; quote it when reporting a problem.
              type: string
            details:
              type: object
              additionalProperties: true
          required:
            - code
            - message
      required:
        - error
      description: Every non-2xx response. 5xx bodies carry only `internal_error` and the request id.
    Money:
      type: object
      properties:
        amount_minor:
          type: integer
        currency:
          type: string
          minLength: 3
          maxLength: 3
        decimals:
          type: integer
          minimum: 0
          maximum: 4
          description: Minor-unit exponent for THIS amount. Dukkan deviates from ISO 4217 (SYP is
            zero-decimal), so always scale by this field, never by an ISO currency table.
      required:
        - amount_minor
        - currency
        - decimals
      description: Integer minor units plus the exponent that scales them. Never a float.
    OrderStatus:
      type: string
      enum:
        - placed
        - approved
        - processing
        - shipped
        - delivered
        - delivered_failed
        - returned
        - cancelled
        - completed
        - partially_refunded
        - refunded
      description: Order lifecycle state. Transitions follow the server matrix; money-bearing states are
        reachable only through refunds.
    SettableOrderStatus:
      type: string
      enum:
        - placed
        - approved
        - processing
        - shipped
        - delivered
        - delivered_failed
        - returned
        - cancelled
      description: Statuses an app may set through PATCH /orders/{id}/status. Money-bearing statuses are
        reachable only through refunds.
    PaymentStatus:
      type: string
      enum:
        - not_required
        - unpaid
        - pending
        - authorized
        - partially_paid
        - paid
        - partially_refunded
        - refunded
        - failed
      description: 'Ledger-projected payment state. Independent of the delivery status: a delivered COD
        order is `unpaid` until the remittance is confirmed.'
    WebhookTopic:
      type: string
      enum:
        - order.created
        - order.status_changed
        - order.paid
        - product.created
        - product.updated
        - product.deleted
        - inventory.movement_created
        - fulfillment.requested
        - fulfillment.created
        - fulfillment.updated
        - refund.created
        - app.uninstalled
      description: Webhook topic. Subscribing needs the read scope that governs the payload;
        `app.uninstalled` needs none and still delivers after revocation.
    WebhookEnvelope:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Event id. Deduplicate on this signed value, never on the delivery header.
        api_version:
          type: string
          const: v1
        topic:
          $ref: '#/components/schemas/WebhookTopic'
        store_id:
          type: string
          format: uuid
        install_id:
          type: string
          format: uuid
          description: The install this delivery was fanned out to. Verify the signature with THIS install's
            secret and refuse a store mismatch.
        sequence:
          type: integer
          exclusiveMinimum: 0
          description: Strictly increasing per store with gaps. Order by it; never count on contiguity.
        occurred_at:
          type: string
          format: date-time
        test:
          description: Present and true only for sandbox test deliveries (POST /webhooks/test). Absent in
            production traffic.
          type: boolean
        data:
          type: object
          additionalProperties: true
      required:
        - id
        - api_version
        - topic
        - store_id
        - install_id
        - sequence
        - occurred_at
        - data
      description: Signed store event. Verify X-Dukkan-Hmac-Sha256 over `{X-Dukkan-Timestamp}.{raw body}`
        with the install's signing secret before parsing.
    Order:
      type: object
      properties:
        id:
          type: string
          format: uuid
        order_number:
          type: string
        status:
          $ref: '#/components/schemas/OrderStatus'
        payment_status:
          $ref: '#/components/schemas/PaymentStatus'
        source:
          type:
            - string
            - 'null'
          description: Sales channel that created the order (`storefront`, `pos`, `admin`, ...).
        line_pricing:
          type: string
          enum:
            - net
            - gross
          description: 'How line totals relate to order totals. "gross": sum of line totals equals subtotal
            and the discount lives on the order. "net" (POS orders): line totals are net of the
            applied promo, so sum of line totals plus discount equals subtotal. Branch on this field
            when reconciling, never on `source`.'
        subtotal:
          $ref: '#/components/schemas/Money'
        discount:
          $ref: '#/components/schemas/Money'
        shipping:
          $ref: '#/components/schemas/Money'
        tax:
          $ref: '#/components/schemas/Money'
        total:
          $ref: '#/components/schemas/Money'
        paid:
          $ref: '#/components/schemas/Money'
        refunded:
          $ref: '#/components/schemas/Money'
        exchange_rate:
          type:
            - object
            - 'null'
          description: 'Snapshot of the rate applied when the order was priced in a non-store currency: rate =
            scaled / 10^scale.'
          properties:
            scaled:
              type: integer
            scale:
              type: integer
              minimum: 0
              maximum: 12
          required:
            - scaled
            - scale
        tags:
          type: array
          items:
            type: string
          description: Merchant and app tags (see PATCH /orders/{id}).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - order_number
        - status
        - payment_status
        - source
        - line_pricing
        - subtotal
        - discount
        - shipping
        - tax
        - total
        - paid
        - refunded
        - exchange_rate
        - tags
        - created_at
        - updated_at
      description: PII-free order. Customer contact fields appear only on the detail endpoint with the
        clients:read scope.
    OrderItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        variant_id:
          type:
            - string
            - 'null'
          format: uuid
          description: Null for custom lines.
        kind:
          type: string
          enum:
            - variant
            - custom
          description: '`custom` lines have no catalog link: they never touch inventory and refund by amount
            only.'
        name:
          type: string
          description: Product name snapshot at order time.
        quantity:
          type: integer
          exclusiveMinimum: 0
        unit_price:
          $ref: '#/components/schemas/Money'
        total:
          $ref: '#/components/schemas/Money'
      required:
        - id
        - variant_id
        - kind
        - name
        - quantity
        - unit_price
        - total
      description: Immutable line snapshot.
    OrderCustomer:
      type: object
      properties:
        first_name:
          type:
            - string
            - 'null'
        last_name:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
      required:
        - first_name
        - last_name
        - email
        - phone
      description: Customer contact details. Requires the `clients:read` scope.
    FulfillmentLine:
      type: object
      properties:
        order_item_id:
          type: string
          format: uuid
        quantity:
          type: integer
          exclusiveMinimum: 0
      required:
        - order_item_id
        - quantity
    Fulfillment:
      type: object
      properties:
        id:
          type: string
          format: uuid
        provider:
          type:
            - string
            - 'null'
        tracking_number:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - pending
            - shipped
            - delivered
            - cancelled
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        lines:
          type: array
          items:
            $ref: '#/components/schemas/FulfillmentLine'
      required:
        - id
        - provider
        - tracking_number
        - status
        - created_at
        - updated_at
        - lines
      description: A shipment covering some or all lines of an order.
    RefundLine:
      type: object
      properties:
        order_item_id:
          type:
            - string
            - 'null'
          format: uuid
        quantity:
          type: integer
          exclusiveMinimum: 0
        amount_minor:
          type: integer
      required:
        - order_item_id
        - quantity
        - amount_minor
    Refund:
      type: object
      properties:
        id:
          type: string
          format: uuid
        amount:
          $ref: '#/components/schemas/Money'
        reason:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        lines:
          type: array
          items:
            $ref: '#/components/schemas/RefundLine'
      required:
        - id
        - amount
        - reason
        - created_at
        - lines
      description: An append-only refund against captured funds.
    OrderReference:
      type: object
      properties:
        kind:
          type: string
          minLength: 1
          maxLength: 40
          description: Record type in the app, e.g. `quote`.
        number:
          type: string
          minLength: 1
          maxLength: 80
          description: Human-readable record number, e.g. `Q-1042`.
        url:
          type:
            - string
            - 'null'
          format: uri
          description: Deep link into the app. Must be https on the app's registered origin; otherwise 422.
          maxLength: 2000
          default: null
      required:
        - kind
        - number
      additionalProperties: false
      description: Link from an order back to the app's own record.
    OrderAttributes:
      type: object
      propertyNames:
        type: string
        pattern: ^[a-z0-9_.-]{1,64}$
      additionalProperties:
        anyOf:
          - anyOf:
              - type: string
                maxLength: 1000
              - type: number
              - type: boolean
              - type: 'null'
          - $ref: '#/components/schemas/OrderReference'
        description: A scalar, or the `reference` object under the `reference` key.
      description: Up to 20 keys (`[a-z0-9_.-]{1,64}`), scalar values up to 1000 UTF-8 bytes, 8192 bytes
        in total. Private to the calling app.
    OrderAttributesPatch:
      type: object
      propertyNames:
        type: string
        pattern: ^[a-z0-9_.-]{1,64}$
      additionalProperties:
        anyOf:
          - anyOf:
              - type: string
                maxLength: 1000
              - type: number
              - type: boolean
              - type: 'null'
          - $ref: '#/components/schemas/OrderReference'
        description: A scalar, or the `reference` object under the `reference` key.
      description: Merged into the app's namespace; a `null` value deletes that key.
    OrderDetail:
      type: object
      properties:
        id:
          type: string
          format: uuid
        order_number:
          type: string
        status:
          $ref: '#/components/schemas/OrderStatus'
        payment_status:
          $ref: '#/components/schemas/PaymentStatus'
        source:
          type:
            - string
            - 'null'
          description: Sales channel that created the order (`storefront`, `pos`, `admin`, ...).
        line_pricing:
          type: string
          enum:
            - net
            - gross
          description: 'How line totals relate to order totals. "gross": sum of line totals equals subtotal
            and the discount lives on the order. "net" (POS orders): line totals are net of the
            applied promo, so sum of line totals plus discount equals subtotal. Branch on this field
            when reconciling, never on `source`.'
        subtotal:
          $ref: '#/components/schemas/Money'
        discount:
          $ref: '#/components/schemas/Money'
        shipping:
          $ref: '#/components/schemas/Money'
        tax:
          $ref: '#/components/schemas/Money'
        total:
          $ref: '#/components/schemas/Money'
        paid:
          $ref: '#/components/schemas/Money'
        refunded:
          $ref: '#/components/schemas/Money'
        exchange_rate:
          type:
            - object
            - 'null'
          description: 'Snapshot of the rate applied when the order was priced in a non-store currency: rate =
            scaled / 10^scale.'
          properties:
            scaled:
              type: integer
            scale:
              type: integer
              minimum: 0
              maximum: 12
          required:
            - scaled
            - scale
        tags:
          type: array
          items:
            type: string
          description: Merchant and app tags (see PATCH /orders/{id}).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'
        fulfillments:
          type: array
          items:
            $ref: '#/components/schemas/Fulfillment'
        refunds:
          type: array
          items:
            $ref: '#/components/schemas/Refund'
        note:
          type:
            - string
            - 'null'
          description: Order note (merchant or app).
        attributes:
          description: The CALLING app's own attributes on this order. Other apps' namespaces are never
            returned.
          $ref: '#/components/schemas/OrderAttributes'
        customer:
          description: Present only when the install holds the `clients:read` scope (customer-PII scope,
            DPA-gated). Absent entirely otherwise.
          $ref: '#/components/schemas/OrderCustomer'
        shipping_address:
          type:
            - object
            - 'null'
          description: Free-form delivery address snapshot. Present only with the `clients:read` scope; absent
            entirely otherwise. May be null when the order has no delivery address (e.g. POS sales).
          additionalProperties: true
      required:
        - id
        - order_number
        - status
        - payment_status
        - source
        - line_pricing
        - subtotal
        - discount
        - shipping
        - tax
        - total
        - paid
        - refunded
        - exchange_rate
        - tags
        - created_at
        - updated_at
        - items
        - fulfillments
        - refunds
        - note
        - attributes
      description: Order with its immutable line snapshots and reconciliation collections.
    Variant:
      type: object
      properties:
        id:
          type: string
          format: uuid
        product_id:
          type: string
          format: uuid
        sku:
          type:
            - string
            - 'null'
        price:
          $ref: '#/components/schemas/Money'
        status:
          type: string
        name:
          type: string
          description: Variant label (e.g. `أحمر / L`).
        attributes:
          type: object
          additionalProperties:
            type: string
          description: "Option name → value, e.g. { color: 'أحمر', size: 'L' }."
        barcode:
          type:
            - string
            - 'null'
        compare_at_price:
          description: Higher strike-through price when set.
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Money'
        image_url:
          type:
            - string
            - 'null'
          description: Main image for this variant (own image, else the product's).
        weight_gram:
          type:
            - integer
            - 'null'
        available_quantity:
          type:
            - integer
            - 'null'
          description: Stock minus reservations across the store's locations. Null when the product is not
            inventory-managed or the install lacks inventory:read.
      required:
        - id
        - product_id
        - sku
        - price
        - status
        - name
        - attributes
        - barcode
        - compare_at_price
        - image_url
        - weight_gram
        - available_quantity
    VariantDetail:
      type: object
      properties:
        id:
          type: string
          format: uuid
        product_id:
          type: string
          format: uuid
        sku:
          type:
            - string
            - 'null'
        price:
          $ref: '#/components/schemas/Money'
        status:
          type: string
        name:
          type: string
          description: Variant label (e.g. `أحمر / L`).
        attributes:
          type: object
          additionalProperties:
            type: string
          description: "Option name → value, e.g. { color: 'أحمر', size: 'L' }."
        barcode:
          type:
            - string
            - 'null'
        compare_at_price:
          description: Higher strike-through price when set.
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Money'
        image_url:
          type:
            - string
            - 'null'
          description: Main image for this variant (own image, else the product's).
        weight_gram:
          type:
            - integer
            - 'null'
        available_quantity:
          type:
            - integer
            - 'null'
          description: Stock minus reservations across the store's locations. Null when the product is not
            inventory-managed or the install lacks inventory:read.
        product_name:
          type: string
        product_status:
          type: string
      required:
        - id
        - product_id
        - sku
        - price
        - status
        - name
        - attributes
        - barcode
        - compare_at_price
        - image_url
        - weight_gram
        - available_quantity
        - product_name
        - product_status
      description: Variant with its product's name and status (GET /products/variants).
    VariantList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/VariantDetail'
      required:
        - data
      description: Batch variant lookup. Unknown ids are omitted, never errors.
    ProductCategory:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
      required:
        - id
        - name
    Product:
      type: object
      properties:
        id:
          type: string
          format: uuid
        dsin:
          type: string
          description: 'Dukkan store item number: the merchant-facing product code.'
        name:
          type: string
        status:
          type: string
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
        image_url:
          type:
            - string
            - 'null'
          description: Main product image.
        image_color:
          type:
            - string
            - 'null'
          description: Dominant colour of the main image (#rrggbb) for placeholders.
        brand:
          type:
            - string
            - 'null'
        category:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/ProductCategory'
        description:
          type:
            - string
            - 'null'
          description: Plain text, at most 500 characters.
        images:
          description: Gallery, present on GET /products/{id} only.
          type: array
          items:
            type: string
        variants:
          type: array
          items:
            $ref: '#/components/schemas/Variant'
      required:
        - id
        - dsin
        - name
        - status
        - updated_at
        - image_url
        - image_color
        - brand
        - category
        - description
        - variants
    Store:
      type: object
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        logo_url:
          type:
            - string
            - 'null'
        banner_urls:
          type: array
          items:
            type: string
        storefront_url:
          type: string
          description: Public https origin; a connected custom domain wins over the platform subdomain.
        whatsapp_number:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
        address:
          type:
            - string
            - 'null'
        accent_color:
          type:
            - string
            - 'null'
          description: Published theme accent, or the store's primary colour.
        locale:
          type: string
        timezone:
          type: string
        country:
          type:
            - string
            - 'null'
        pricing_currency:
          type: object
          properties:
            code:
              type: string
              minLength: 3
              maxLength: 3
            decimals:
              type: integer
              minimum: 0
              maximum: 4
          required:
            - code
            - decimals
          description: The currency catalog prices are stored in.
        storefront_currencies:
          type: object
          properties:
            allowed:
              type: array
              items:
                type: string
                minLength: 3
                maxLength: 3
            default:
              type: string
              minLength: 3
              maxLength: 3
          required:
            - allowed
            - default
          description: Currencies a customer may pay in; POST /orders accepts these only.
        fx:
          type:
            - object
            - 'null'
          description: Store-adjusted rates from the pricing currency, exactly what checkout applies; includes
            the base at identity. Null when no rate table is available.
          properties:
            base:
              type: string
              minLength: 3
              maxLength: 3
            as_of:
              type: string
              format: date-time
            rates:
              type: object
              additionalProperties:
                type: object
                properties:
                  scaled:
                    type: integer
                    exclusiveMinimum: 0
                  scale:
                    type: integer
                    minimum: 0
                    maximum: 12
                required:
                  - scaled
                  - scale
                description: rate = scaled / 10^scale.
          required:
            - base
            - as_of
            - rates
        payment_methods:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                enum:
                  - cod
                  - shamcash_manual
              shamcash_id:
                description: 'Present for shamcash_manual: the pay-to id shown at checkout.'
                type: string
            required:
              - id
          description: Enabled manual payment methods.
        provinces:
          type: array
          items:
            type: object
            properties:
              value:
                type: string
              ar:
                type: string
              en:
                type: string
            required:
              - value
              - ar
              - en
          description: Accepted `shipping_address.province` values with labels.
      required:
        - id
        - slug
        - name
        - description
        - logo_url
        - banner_urls
        - storefront_url
        - whatsapp_number
        - phone
        - address
        - accent_color
        - locale
        - timezone
        - country
        - pricing_currency
        - storefront_currencies
        - fx
        - payment_methods
        - provinces
      description: Public identity and checkout facts of the token's store.
    Discount:
      type: object
      properties:
        id:
          type: string
          format: uuid
        method:
          type: string
          enum:
            - code
            - automatic
        code:
          type:
            - string
            - 'null'
        title:
          type:
            - string
            - 'null'
        value_type:
          type: string
          enum:
            - percentage
            - fixed_amount
            - free_shipping
            - buy_x_get_y
        target:
          type: string
          enum:
            - order
            - products
            - categories
        percent:
          type:
            - number
            - 'null'
          description: Set only for percentage discounts; `amount` only for fixed_amount.
        amount:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Money'
        is_active:
          type: boolean
        usage_limit_total:
          type:
            - integer
            - 'null'
        used_count:
          type: integer
          minimum: 0
        applies_to_pos:
          type: boolean
        applies_to_online:
          type: boolean
        starts_at:
          type:
            - string
            - 'null'
          format: date-time
        ends_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
      required:
        - id
        - method
        - code
        - title
        - value_type
        - target
        - percent
        - amount
        - is_active
        - usage_limit_total
        - used_count
        - applies_to_pos
        - applies_to_online
        - starts_at
        - ends_at
        - created_at
        - updated_at
      description: Read-only view of a discount. Redemption accounting stays first-party.
    Movement:
      type: object
      properties:
        id:
          type: string
          format: uuid
        sequence:
          type: integer
          minimum: 0
          description: Monotonic insert sequence; the stable sort key for what happened in which order.
        variant_id:
          type:
            - string
            - 'null'
          format: uuid
        location_id:
          type: string
          format: uuid
        delta:
          type: integer
        stock_after:
          type: integer
        reason:
          type: string
        occurred_at:
          type: string
          format: date-time
      required:
        - id
        - sequence
        - variant_id
        - location_id
        - delta
        - stock_after
        - reason
        - occurred_at
      description: One append-only stock movement.
    WebhookSubscription:
      type: object
      properties:
        id:
          type: string
          format: uuid
        endpoint_url:
          type: string
        topics:
          type: array
          items:
            $ref: '#/components/schemas/WebhookTopic'
        status:
          type: string
          enum:
            - active
            - disabled
          description: '`disabled` = quarantined after sustained delivery failures; re-PUT the subscription to
            resume.'
        consecutive_failures:
          type: integer
          minimum: 0
        activated_at:
          type: string
          format: date-time
          description: Events that occurred before this instant are never delivered to this subscription.
        disabled_at:
          type:
            - string
            - 'null'
          format: date-time
        secret:
          description: Present only on the PUT response that minted or rotated it. Store it immediately; it
            can never be read back.
          type: string
      required:
        - id
        - endpoint_url
        - topics
        - status
        - consecutive_failures
        - activated_at
        - disabled_at
      description: One subscription per install; PUT is a full replace.
    Installation:
      type: object
      properties:
        install:
          type: object
          properties:
            id:
              type: string
              format: uuid
            app_id:
              type: string
              format: uuid
            status:
              type: string
              enum:
                - active
                - suspended
              description: A token only resolves for an active install; `suspended` is reserved for billing holds
                surfaced through this document.
            granted_scopes:
              type: array
              items:
                type: string
              description: Scopes the merchant consented to.
            effective_scopes:
              type: array
              items:
                type: string
              description: Granted scopes intersected with the grantor's live permissions. This is what every
                request is checked against.
            distribution_channel:
              type: string
              enum:
                - development
                - private
                - public
            installed_at:
              type: string
              format: date-time
            api_version:
              type: string
              description: Dated v1 revision this server speaks (same value as X-Dukkan-Api-Version).
            webhook:
              type:
                - object
                - 'null'
              description: The active subscription, or null when none is configured.
              properties:
                id:
                  type: string
                  format: uuid
                endpoint_url:
                  type: string
                topics:
                  type: array
                  items:
                    $ref: '#/components/schemas/WebhookTopic'
                status:
                  type: string
                  enum:
                    - active
                    - disabled
                  description: '`disabled` = quarantined after sustained delivery failures; re-PUT the subscription to
                    resume.'
                consecutive_failures:
                  type: integer
                  minimum: 0
                activated_at:
                  type: string
                  format: date-time
                  description: Events that occurred before this instant are never delivered to this subscription.
                disabled_at:
                  type:
                    - string
                    - 'null'
                  format: date-time
              required:
                - id
                - endpoint_url
                - topics
                - status
                - consecutive_failures
                - activated_at
                - disabled_at
          required:
            - id
            - app_id
            - status
            - granted_scopes
            - effective_scopes
            - distribution_channel
            - installed_at
            - api_version
            - webhook
        store:
          type: object
          properties:
            id:
              type: string
              format: uuid
            slug:
              type: string
            name:
              type: string
            currency:
              type: string
              minLength: 3
              maxLength: 3
            decimals:
              type: integer
              minimum: 0
              maximum: 4
              description: Minor-unit exponent of the store currency.
            locale:
              type: string
              description: Storefront locale (`ar` or `en`).
            timezone:
              type: string
              description: IANA zone derived from the store country; use it for day boundaries in reports.
            country:
              type:
                - string
                - 'null'
              description: ISO 3166-1 alpha-2 when the merchant set one.
            is_development:
              type: boolean
              description: True for sandbox stores. Test deliveries and dev sessions exist only here.
          required:
            - id
            - slug
            - name
            - currency
            - decimals
            - locale
            - timezone
            - country
            - is_development
      required:
        - install
        - store
      description: The install and the store it is bound to, as the platform sees them.
    WebhookTestEvent:
      type: object
      properties:
        event_id:
          type: string
          format: uuid
        topic:
          $ref: '#/components/schemas/WebhookTopic'
        sequence:
          type: integer
          exclusiveMinimum: 0
        occurred_at:
          type: string
          format: date-time
        test:
          type: boolean
          const: true
        data:
          type: object
          additionalProperties: true
          description: The payload that was written, after overrides and validation.
        deliveries:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
                description: Delivery id; also sent as X-Dukkan-Delivery-Id.
              status:
                type: string
                enum:
                  - pending
                  - delivering
                  - retry_scheduled
                  - succeeded
                  - dead
              endpoint_url:
                type: string
            required:
              - id
              - status
              - endpoint_url
          description: One row per subscription the event fanned out to; the worker sends within about a minute.
      required:
        - event_id
        - topic
        - sequence
        - occurred_at
        - test
        - data
        - deliveries
      description: The accepted test event and its queued deliveries.
    OrderPage:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Order'
        next_cursor:
          type:
            - string
            - 'null'
          description: Pass back as `cursor` to fetch the next page; null on the last page.
      required:
        - data
        - next_cursor
      description: Cursor page of PII-free orders.
    ProductPage:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Product'
        next_cursor:
          type:
            - string
            - 'null'
          description: Pass back as `cursor` to fetch the next page; null on the last page.
      required:
        - data
        - next_cursor
      description: Cursor page of products with variants.
    DiscountPage:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Discount'
        next_cursor:
          type:
            - string
            - 'null'
          description: Pass back as `cursor` to fetch the next page; null on the last page.
      required:
        - data
        - next_cursor
      description: Cursor page of discounts.
    MovementPage:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Movement'
        next_cursor:
          type:
            - string
            - 'null'
          description: Pass back as `cursor` to fetch the next page; null on the last page.
      required:
        - data
        - next_cursor
      description: Cursor page of append-only inventory movements.
    CreateOrderItem:
      type: object
      properties:
        variant_id:
          type:
            - string
            - 'null'
          format: uuid
          description: Catalog variant, or null for a custom line.
        name:
          description: Line label. Required when variant_id is null; ignored otherwise (the catalog name is
            snapshotted).
          type: string
          minLength: 1
          maxLength: 200
        quantity:
          type: integer
          minimum: 1
          maximum: 100000
        unit_price_minor:
          type: integer
          minimum: 0
          maximum: 10000000000000
          description: Agreed unit price in minor units of the order currency. Zero is allowed.
      required:
        - variant_id
        - quantity
        - unit_price_minor
      additionalProperties: false
      description: One order line with the price the merchant agreed.
    CreateOrderRequest:
      type: object
      properties:
        currency:
          description: One of the store's allowed storefront currencies; otherwise 422 `currency_not_allowed`.
          type: string
          minLength: 3
          maxLength: 3
        status:
          default: placed
          description: '`placed` (default) lets the merchant review; `approved` records an explicit customer
            acceptance the app collected.'
          type: string
          enum:
            - placed
            - approved
        items:
          minItems: 1
          maxItems: 200
          type: array
          items:
            $ref: '#/components/schemas/CreateOrderItem'
        discount:
          type:
            - object
            - 'null'
          description: Order-level discount; must not exceed the sum of the lines.
          properties:
            amount_minor:
              type: integer
            title:
              type:
                - string
                - 'null'
              maxLength: 80
              default: null
          required:
            - amount_minor
          additionalProperties: false
          default: null
        shipping:
          type: object
          properties:
            amount_minor:
              type: integer
            method:
              type: string
              enum:
                - delivery
                - pickup
          required:
            - amount_minor
            - method
          additionalProperties: false
        customer:
          type: object
          properties:
            name:
              type: string
              minLength: 1
              maxLength: 120
            phone:
              type: string
              minLength: 5
              maxLength: 32
              description: Full international number with country code; stored normalised.
            email:
              type:
                - string
                - 'null'
              format: email
              description: Optional. Never invented when absent.
              maxLength: 254
              default: null
          required:
            - name
            - phone
          additionalProperties: false
        shipping_address:
          type:
            - object
            - 'null'
          description: Required when shipping.method is `delivery`.
          properties:
            address:
              type: string
              minLength: 1
              maxLength: 500
            province:
              type: string
              maxLength: 40
              description: Syrian province value, e.g. `damascus` (see GET /store `provinces`).
            city:
              type:
                - string
                - 'null'
              maxLength: 80
              default: null
            notes:
              type:
                - string
                - 'null'
              maxLength: 500
              default: null
          required:
            - address
            - province
          additionalProperties: false
          default: null
        payment_method:
          type: string
          enum:
            - cod
            - shamcash_manual
          description: Must be enabled in the store; otherwise 409 `payment_method_disabled`.
        exchange_rate:
          type:
            - object
            - 'null'
          description: Required when currency differs from the store's pricing currency, forbidden otherwise.
            rate = scaled / 10^scale, oriented 1 pricing-currency unit = rate order-currency units
            (same as GET /store `fx`).
          properties:
            scaled:
              type: integer
              exclusiveMinimum: 0
            scale:
              type: integer
              minimum: 0
              maximum: 12
          required:
            - scaled
            - scale
          additionalProperties: false
          default: null
        note:
          type:
            - string
            - 'null'
          maxLength: 2000
          default: null
        tags:
          description: At most 10 tags after case-insensitive dedupe.
          maxItems: 10
          type: array
          items:
            description: 'Free-text tag: Unicode letters, marks, digits, space, _ and -, 1 to 40 characters.
              Normalised (NFC, trimmed) and deduped case-insensitively.'
            type: string
            maxLength: 200
        attributes:
          default: {}
          $ref: '#/components/schemas/OrderAttributes'
        inventory:
          default: decrement
          type: string
          enum:
            - decrement
            - skip
      required:
        - currency
        - items
        - shipping
        - customer
        - payment_method
      additionalProperties: false
      description: 'Create an order with app-set prices. Totals are recomputed server-side: subtotal = Σ
        quantity × unit_price_minor, total = subtotal − discount + shipping, tax 0.'
    UpdateOrderRequest:
      type: object
      properties:
        note:
          type:
            - string
            - 'null'
          description: Replace the order note; null clears it.
          maxLength: 2000
        tags:
          description: "Set operations, never a replace: the merchant's and other apps' tags survive."
          type: object
          properties:
            add:
              description: At most 10 tags after case-insensitive dedupe.
              maxItems: 10
              type: array
              items:
                description: 'Free-text tag: Unicode letters, marks, digits, space, _ and -, 1 to 40 characters.
                  Normalised (NFC, trimmed) and deduped case-insensitively.'
                type: string
                maxLength: 200
            remove:
              description: Removed case-insensitively.
              maxItems: 10
              type: array
              items:
                type: string
                maxLength: 200
          additionalProperties: false
        attributes:
          $ref: '#/components/schemas/OrderAttributesPatch'
      additionalProperties: false
      minProperties: 1
      description: Update the note, add/remove tags, or merge the app's own attributes. Money, lines,
        customer and address are immutable after creation.
    UpdateOrderStatusRequest:
      type: object
      properties:
        status:
          $ref: '#/components/schemas/SettableOrderStatus'
      required:
        - status
      description: Transition an order through the server matrix.
    CreateFulfillmentRequest:
      type: object
      properties:
        provider:
          type:
            - string
            - 'null'
          maxLength: 100
        tracking_number:
          type:
            - string
            - 'null'
          maxLength: 200
        status:
          default: shipped
          description: '"pending" reserves the quantities without auto-shipping the order (carrier booked,
            pickup not yet made); move to "shipped" via PATCH when the parcel leaves.'
          type: string
          enum:
            - pending
            - shipped
        lines:
          minItems: 1
          maxItems: 100
          type: array
          items:
            type: object
            properties:
              order_item_id:
                type: string
                format: uuid
              quantity:
                type: integer
                exclusiveMinimum: 0
            required:
              - order_item_id
              - quantity
            additionalProperties: false
      required:
        - lines
      additionalProperties: false
      description: Fulfill selected line quantities. Each order item may appear once.
    UpdateFulfillmentRequest:
      type: object
      properties:
        tracking_number:
          type:
            - string
            - 'null'
          maxLength: 200
        status:
          type: string
          enum:
            - shipped
            - delivered
            - cancelled
        cod_collected_minor:
          description: 'COD remittance PROPOSAL: the cash the courier reports collecting (minor units; may be
            less than the order total). Moves no money. The merchant confirms in admin, and that
            confirmation writes the cod_collection ledger row which updates payment status and fires
            order.paid. One open proposal per order; re-sending updates it.'
          type: integer
          exclusiveMinimum: 0
        cod_collected_note:
          type: string
          maxLength: 300
      additionalProperties: false
      minProperties: 1
      description: Update tracking, mark delivered, cancel, or propose a COD remittance. At least one
        field is required.
    CreateRefundRequest:
      type: object
      properties:
        amount_minor:
          type: integer
          exclusiveMinimum: 0
        currency:
          description: Optional confirmation guard; 409 when it differs from the order's currency.
          type: string
          minLength: 3
          maxLength: 3
        reason:
          type:
            - string
            - 'null'
          maxLength: 500
        lines:
          description: Optional line breakdown; amounts must sum to amount_minor.
          minItems: 1
          maxItems: 100
          type: array
          items:
            type: object
            properties:
              order_item_id:
                type: string
                format: uuid
              quantity:
                type: integer
                exclusiveMinimum: 0
              amount_minor:
                type: integer
                exclusiveMinimum: 0
            required:
              - order_item_id
              - quantity
              - amount_minor
            additionalProperties: false
        restock:
          description: Return the refunded quantities to stock. Requires `lines` and the inventory:write
            scope. Untracked products are skipped.
          type: object
          properties:
            location_id:
              type: string
              format: uuid
          required:
            - location_id
          additionalProperties: false
      required:
        - amount_minor
      additionalProperties: false
      description: Append a refund against captured funds.
    CreateInventoryMovementRequest:
      type: object
      properties:
        variant_id:
          type: string
          format: uuid
        location_id:
          type: string
          format: uuid
        delta:
          type: integer
          minimum: -1000000
          maximum: 1000000
          not:
            const: 0
          description: Signed quantity change. Absolute stock writes are forbidden.
        reason:
          type: string
          enum:
            - correction
            - initial
            - return
            - damage
        note:
          type:
            - string
            - 'null'
          maxLength: 500
      required:
        - variant_id
        - location_id
        - delta
        - reason
      description: Append an inventory movement.
    UpsertWebhookSubscriptionRequest:
      type: object
      properties:
        endpoint_url:
          type: string
          maxLength: 2000
          format: uri
          description: Public HTTPS URL. Private, loopback and IP-literal hosts are refused.
        topics:
          minItems: 1
          maxItems: 12
          type: array
          items:
            $ref: '#/components/schemas/WebhookTopic'
        rotate_secret:
          description: Mint a new signing secret and return it once in the response.
          type: boolean
      required:
        - endpoint_url
        - topics
      additionalProperties: false
      description: Create or replace the install's single subscription.
    CreateWebhookTestEventRequest:
      type: object
      properties:
        topic:
          $ref: '#/components/schemas/WebhookTopic'
        data:
          description: Optional overrides merged over the platform-built sample payload for the topic (at most
            16 KB).
          type: object
          additionalProperties: true
      required:
        - topic
      additionalProperties: false
      description: Fire one signed test delivery (sandbox stores only).
    OrderCreatedEventData:
      type: object
      properties:
        order_id:
          type: string
          format: uuid
        order_number:
          type: string
        status:
          type: string
          description: Order status at creation (usually `placed`).
        total_minor:
          type: integer
          description: Integer minor units of `currency` (scale by the store's decimals).
        currency:
          type: string
          minLength: 3
          maxLength: 3
        source:
          type:
            - string
            - 'null'
          description: Sales channel (`storefront`, `pos`, `admin`, `app`). Emitted since revision 2026-09-10.
      required:
        - order_id
        - order_number
        - status
        - total_minor
        - currency
      additionalProperties: true
    OrderStatusChangedEventData:
      type: object
      properties:
        order_id:
          type: string
          format: uuid
        from_status:
          type: string
        status:
          type: string
          description: The status the order moved to.
      required:
        - order_id
        - from_status
        - status
      additionalProperties: true
    OrderPaidEventData:
      type: object
      properties:
        order_id:
          type: string
          format: uuid
        amount_minor:
          type: integer
          description: Captured funds at the moment they crossed the order total.
        currency:
          type: string
          minLength: 3
          maxLength: 3
      required:
        - order_id
        - amount_minor
        - currency
      additionalProperties: true
      description: Fires when captured funds reach the order total, from the payments ledger only. COD
        orders reach it when the merchant confirms the courier's remittance, never on delivery.
    ProductCreatedEventData:
      type: object
      properties:
        product_id:
          type: string
          format: uuid
      required:
        - product_id
      additionalProperties: true
    ProductUpdatedEventData:
      type: object
      properties:
        product_id:
          type: string
          format: uuid
        variant_id:
          description: Set when a variant row changed rather than the product itself.
          type: string
          format: uuid
        operation:
          description: 'Present on variant changes: which write on the variant fired the event.'
          type: string
          enum:
            - insert
            - update
            - delete
      required:
        - product_id
      additionalProperties: true
    ProductDeletedEventData:
      type: object
      properties:
        product_id:
          type: string
          format: uuid
      required:
        - product_id
      additionalProperties: true
    InventoryMovementCreatedEventData:
      type: object
      properties:
        movement_id:
          type: string
          format: uuid
        variant_id:
          type:
            - string
            - 'null'
          format: uuid
        location_id:
          type: string
          format: uuid
        delta:
          type: integer
        stock_after:
          type: integer
        reason:
          type: string
      required:
        - movement_id
        - variant_id
        - location_id
        - delta
        - stock_after
        - reason
      additionalProperties: true
    FulfillmentRequestedEventData:
      type: object
      properties:
        fulfillment_id:
          type: string
          format: uuid
        order_id:
          type: string
          format: uuid
        lines:
          type: array
          items:
            type: object
            properties:
              order_item_id:
                type: string
                format: uuid
              quantity:
                type: integer
                exclusiveMinimum: 0
            required:
              - order_item_id
              - quantity
            additionalProperties: true
      required:
        - fulfillment_id
        - order_id
        - lines
      additionalProperties: true
      description: 'A fulfillment was created as `pending`: a pickup is waiting to be booked by a carrier app.'
    FulfillmentCreatedEventData:
      type: object
      properties:
        fulfillment_id:
          type: string
          format: uuid
        order_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - shipped
        tracking_number:
          type:
            - string
            - 'null'
        lines:
          type: array
          items:
            type: object
            properties:
              order_item_id:
                type: string
                format: uuid
              quantity:
                type: integer
                exclusiveMinimum: 0
            required:
              - order_item_id
              - quantity
            additionalProperties: true
        order_fully_fulfilled:
          type: boolean
          description: True when every line of the order is now covered by a fulfillment.
      required:
        - fulfillment_id
        - order_id
        - status
        - tracking_number
        - lines
        - order_fully_fulfilled
      additionalProperties: true
    FulfillmentUpdatedEventData:
      type: object
      properties:
        fulfillment_id:
          type: string
          format: uuid
        order_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - shipped
            - delivered
            - cancelled
        previous_status:
          type: string
        tracking_number:
          type:
            - string
            - 'null'
      required:
        - fulfillment_id
        - order_id
        - status
        - previous_status
        - tracking_number
      additionalProperties: true
    RefundCreatedEventData:
      type: object
      properties:
        refund_id:
          type: string
          format: uuid
        order_id:
          type: string
          format: uuid
        amount_minor:
          type: integer
          description: Integer minor units of `currency` (scale by the store's decimals).
        currency:
          type: string
          minLength: 3
          maxLength: 3
        lines:
          description: Line breakdown when the refund carried one. Absent on POS refunds.
          type: array
          items:
            type: object
            properties:
              order_item_id:
                type:
                  - string
                  - 'null'
                format: uuid
              quantity:
                type: integer
                exclusiveMinimum: 0
              amount_minor:
                type: integer
                description: Integer minor units of `currency` (scale by the store's decimals).
            required:
              - order_item_id
              - quantity
              - amount_minor
            additionalProperties: true
        source:
          description: Present when the refund was written at the point of sale.
          type: string
          const: pos
      required:
        - refund_id
        - order_id
        - amount_minor
        - currency
      additionalProperties: true
    AppUninstalledEventData:
      type: object
      properties:
        app_id:
          type: string
          format: uuid
        install_id:
          type: string
          format: uuid
      required:
        - app_id
        - install_id
      additionalProperties: true
      description: 'Farewell notice: the merchant removed the app. Tokens are already dead; delete what
        the DPA requires and stop calling the API for this install.'
x-dukkan-api-version: 2026-09-10
x-dukkan-topic-scopes:
  order.created: orders:read
  order.status_changed: orders:read
  order.paid: orders:read
  product.created: products:read
  product.updated: products:read
  product.deleted: products:read
  inventory.movement_created: inventory:read
  fulfillment.requested: orders:read
  fulfillment.created: orders:read
  fulfillment.updated: orders:read
  refund.created: orders:read
  app.uninstalled: null
x-dukkan-topic-payloads:
  order.created:
    $ref: '#/components/schemas/OrderCreatedEventData'
  order.status_changed:
    $ref: '#/components/schemas/OrderStatusChangedEventData'
  order.paid:
    $ref: '#/components/schemas/OrderPaidEventData'
  product.created:
    $ref: '#/components/schemas/ProductCreatedEventData'
  product.updated:
    $ref: '#/components/schemas/ProductUpdatedEventData'
  product.deleted:
    $ref: '#/components/schemas/ProductDeletedEventData'
  inventory.movement_created:
    $ref: '#/components/schemas/InventoryMovementCreatedEventData'
  fulfillment.requested:
    $ref: '#/components/schemas/FulfillmentRequestedEventData'
  fulfillment.created:
    $ref: '#/components/schemas/FulfillmentCreatedEventData'
  fulfillment.updated:
    $ref: '#/components/schemas/FulfillmentUpdatedEventData'
  refund.created:
    $ref: '#/components/schemas/RefundCreatedEventData'
  app.uninstalled:
    $ref: '#/components/schemas/AppUninstalledEventData'
x-dukkan-order-status-transitions:
  placed:
    - approved
    - processing
    - shipped
    - delivered
    - cancelled
  approved:
    - processing
    - shipped
    - delivered
    - cancelled
  processing:
    - shipped
    - delivered
    - cancelled
  shipped:
    - delivered
    - delivered_failed
  delivered: []
  delivered_failed:
    - shipped
    - returned
  returned: []
  cancelled: []
  completed:
    - cancelled
  partially_refunded:
    - cancelled
  refunded: []
