openapi: 3.0.3
info:
  title: MODR Open API
  version: "2026-08-17"
  description: |
    Public REST API for partner integrations against a single MODR outlet.
    Auth uses per-outlet API keys (`modr_live_…` / `modr_test_…`).

    `POST /orders` lands in Orderhanterare (waiting queue). `POST /checks` creates
    an open POS check. Payments, refunds, cash, Z/X/fiscal, staff PIN and device
    link are out of scope for v1.

    Write endpoints accept `Idempotency-Key` (8–128 characters). Replays return
    the original status and body.
servers:
  - url: https://api.modr.se/v1
    description: MODR Open API (canonical)
  - url: https://modr.se/api/v1
    description: Same API via modr.se
security:
  - BearerAuth: []
  - ApiKeyAuth: []
tags:
  - name: Meta
  - name: Products
  - name: Tables
  - name: Checks
  - name: Orders
  - name: Sales
paths:
  /version:
    get:
      tags: [Meta]
      security: []
      summary: API version
      responses:
        "200":
          description: Version payload
          content:
            application/json:
              schema:
                type: object
                properties:
                  version: { type: string }
                  product: { type: string }
  /environment:
    get:
      tags: [Meta]
      summary: Key environment and outlet binding
      responses:
        "200":
          description: Environment
          content:
            application/json:
              schema:
                type: object
                properties:
                  environment: { type: string, enum: [live, test] }
                  outletId: { type: string }
                  tenantId: { type: string }
                  scopes:
                    type: array
                    items: { type: string }
  /products:
    get:
      tags: [Products]
      summary: Published menu/catalog for the outlet
      description: Includes modifier groups so partners can send required options on orders and checks.
      responses:
        "200":
          description: Product catalog
          content:
            application/json:
              schema:
                type: object
                properties:
                  menuVersion: { type: number, nullable: true }
                  products:
                    type: array
                    items:
                      $ref: "#/components/schemas/Product"
  /tables:
    get:
      tags: [Tables]
      summary: Floor tables and current open check
      responses:
        "200":
          description: Tables
          content:
            application/json:
              schema:
                type: object
                properties:
                  tables:
                    type: array
                    items:
                      $ref: "#/components/schemas/Table"
  /checks:
    get:
      tags: [Checks]
      summary: List open POS checks
      parameters:
        - in: query
          name: limit
          schema: { type: integer, default: 50, maximum: 100 }
      responses:
        "200":
          description: Open checks
    post:
      tags: [Checks]
      summary: Create an open POS check (no payment)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [lines]
              properties:
                name: { type: string }
                orderType: { type: string, description: "Ät här or Takeaway" }
                tableId: { type: string }
                lines:
                  type: array
                  items:
                    $ref: "#/components/schemas/LineInput"
      responses:
        "201":
          description: Created check
        "409":
          description: Table occupied or idempotency conflict
  /checks/{id}:
    get:
      tags: [Checks]
      summary: Get one open check
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Check
        "404":
          description: Not found
  /checks/{id}/items:
    post:
      tags: [Checks]
      summary: Add items to an open check
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [lines]
              properties:
                lines:
                  type: array
                  items:
                    $ref: "#/components/schemas/LineInput"
      responses:
        "200":
          description: Updated check
        "409":
          description: Check locked in payment
  /checks/{id}/table:
    put:
      tags: [Checks]
      summary: Attach an open check to a table
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tableId]
              properties:
                tableId: { type: string }
      responses:
        "200":
          description: Updated check
        "409":
          description: Table occupied
  /orders:
    get:
      tags: [Orders]
      summary: List Orderhanterare queue orders
      parameters:
        - in: query
          name: queue
          schema: { type: string, enum: [waiting, current] }
      responses:
        "200":
          description: Queue orders
    post:
      tags: [Orders]
      summary: Create an Orderhanterare waiting order
      description: Does not open a POS check. Staff accept/prepare/complete it in Orderhanterare.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [lines]
              properties:
                name: { type: string }
                orderType: { type: string }
                tableId: { type: string }
                notes: { type: string }
                lines:
                  type: array
                  items:
                    $ref: "#/components/schemas/LineInput"
      responses:
        "201":
          description: Created queue order
  /sales:
    get:
      tags: [Sales]
      summary: List closed sales
      parameters:
        - in: query
          name: from
          schema: { type: string, format: date-time }
        - in: query
          name: to
          schema: { type: string, format: date-time }
        - in: query
          name: limit
          schema: { type: integer, default: 50, maximum: 100 }
        - in: query
          name: cursor
          schema: { type: string }
      responses:
        "200":
          description: Sales page
          content:
            application/json:
              schema:
                type: object
                properties:
                  sales:
                    type: array
                    items:
                      $ref: "#/components/schemas/Sale"
                  nextCursor: { type: string, nullable: true }
  /sales/{id}:
    get:
      tags: [Sales]
      summary: Get one closed sale
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Sale
        "404":
          description: Not found
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: Authorization Bearer modr_live_… or modr_test_…
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: false
      schema: { type: string, minLength: 8, maxLength: 128 }
  schemas:
    Error:
      type: object
      properties:
        code: { type: string }
        message: { type: string }
        correlationId: { type: string }
    LineInput:
      type: object
      required: [productId, qty]
      properties:
        productId: { type: string }
        qty: { type: number, maximum: 99 }
        modifierOptionIds:
          type: array
          items: { type: string }
    Product:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        price: { type: number }
        vatRate: { type: number }
        categoryId: { type: string, nullable: true }
        categoryLabel: { type: string, nullable: true }
        active: { type: boolean }
        modifierGroups:
          type: array
          items: { type: object, additionalProperties: true }
    Table:
      type: object
      properties:
        id: { type: string }
        label: { type: string }
        sectionId: { type: string, nullable: true }
        sectionName: { type: string, nullable: true }
        seats: { type: number }
        openCheckId: { type: string, nullable: true }
    Sale:
      type: object
      additionalProperties: true
      properties:
        id: { type: string }
        ref: { type: string, nullable: true }
        total: { type: number }
        closedAt: { type: string, nullable: true }
        orderType: { type: string, nullable: true }
        source: { type: string, nullable: true }
        lines: { type: array, items: { type: object, additionalProperties: true } }
        payments: { type: array, items: { type: object, additionalProperties: true } }
        vat: { type: array, items: { type: object, additionalProperties: true } }
x-webhooks:
  check.created:
    post:
      summary: Fired when an open check is created via Open API
  check.updated:
    post:
      summary: Fired when items are added or a table is attached
  order.created:
    post:
      summary: Fired when an Orderhanterare queue order is created via Open API
  sale.created:
    post:
      summary: Fired when a check is archived/closed as a sale
      description: |
        Payload includes lines, VAT and payments. Headers: X-Modr-Timestamp,
        X-Modr-Signature (HMAC-SHA256 of `{timestamp}.{rawBody}`), X-Modr-Event.
        Failed deliveries are retried with backoff up to 8 attempts.
