openapi: 3.1.0
info:
  title: Posific Developer API
  version: 2026-09-22
  description: >-
    Server-to-server catalog, pricing, inventory-count and change-feed API for
    merchant-owned integrations. Money is integer minor units and quantity is
    integer thousandths. Online orders and payments are not part of this version.
servers:
  - url: https://posific.com/api/integrations/v1
security:
  - bearerApiKey: []
tags:
  - name: Store
  - name: Categories
  - name: Products
  - name: Inventory
  - name: Events
paths:
  /:
    get:
      operationId: getApiContext
      tags: [Store]
      summary: Read the tenant, location and credential capabilities
      responses:
        '200':
          description: Bound store context
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiContext'
        default:
          $ref: '#/components/responses/Error'
  /categories:
    get:
      operationId: listCategories
      tags: [Categories]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - in: query
          name: include_archived
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: Category page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoryPage'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createCategory
      tags: [Categories]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryCreate'
      responses:
        '201':
          description: Created category
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Category' }
        default:
          $ref: '#/components/responses/Error'
  /categories/{category_id}:
    parameters:
      - in: path
        name: category_id
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getCategory
      tags: [Categories]
      responses:
        '200':
          description: Category
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Category' }
        default: { $ref: '#/components/responses/Error' }
    patch:
      operationId: updateCategory
      tags: [Categories]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CategoryUpdate'
      responses:
        '200':
          description: Updated category
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Category' }
        default: { $ref: '#/components/responses/Error' }
  /products:
    get:
      operationId: listProducts
      tags: [Products]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - in: query
          name: include_archived
          schema: { type: boolean, default: false }
        - in: query
          name: published_only
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: Product page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductPage'
        default: { $ref: '#/components/responses/Error' }
    post:
      operationId: createProduct
      tags: [Products]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductCreate'
      responses:
        '201':
          description: Created product
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Product' }
        default: { $ref: '#/components/responses/Error' }
  /products/{product_id}:
    parameters:
      - in: path
        name: product_id
        required: true
        schema: { type: string, format: uuid }
    get:
      operationId: getProduct
      tags: [Products]
      responses:
        '200':
          description: Product
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Product' }
        default: { $ref: '#/components/responses/Error' }
    patch:
      operationId: updateProduct
      tags: [Products]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductUpdate'
      responses:
        '200':
          description: Updated product
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: '#/components/schemas/Product' }
        default: { $ref: '#/components/responses/Error' }
  /inventory:
    get:
      operationId: listInventory
      tags: [Inventory]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Inventory position page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InventoryPage'
        default: { $ref: '#/components/responses/Error' }
  /inventory/adjustments:
    post:
      operationId: recordPhysicalCount
      tags: [Inventory]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InventoryAdjustment'
      responses:
        '201':
          description: Appended count result
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { type: object, additionalProperties: true }
        default: { $ref: '#/components/responses/Error' }
  /events:
    get:
      operationId: listEvents
      tags: [Events]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Ordered Catalog and Inventory events
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventPage'
        default: { $ref: '#/components/responses/Error' }
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: PosificAPIKey
  parameters:
    Limit:
      in: query
      name: limit
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    Cursor:
      in: query
      name: cursor
      description: Opaque cursor returned by the prior page
      schema: { type: string }
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema: { type: string, format: uuid }
  responses:
    Error:
      description: Stable error envelope
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    ApiContext:
      type: object
      required: [api_version, capabilities, store]
      properties:
        api_version: { type: string, const: '2026-09-22' }
        capabilities:
          type: array
          items: { type: string }
        store: { $ref: '#/components/schemas/Store' }
    Store:
      type: object
      required: [id, name, country_code, currency_code, default_locale, time_zone, location]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        country_code: { type: string, pattern: '^[A-Z]{2}$' }
        currency_code: { type: string, pattern: '^[A-Z]{3}$' }
        default_locale: { type: string, enum: [en, ar] }
        time_zone: { type: string }
        location:
          type: object
          required: [id, name]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
    PageBase:
      type: object
      required: [data, next_cursor]
      properties:
        next_cursor: { type: [string, 'null'] }
    Category:
      type: object
      required: [id, name, color, is_default, state, revision, updated_at]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 2, maxLength: 80 }
        color: { type: string, pattern: '^#[0-9A-Fa-f]{6}$' }
        is_default: { type: boolean }
        state: { type: string, enum: [active, archived] }
        revision: { type: integer, minimum: 1 }
        updated_at: { type: string, format: date-time }
    CategoryCreate:
      type: object
      required: [id, name]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 2, maxLength: 80 }
        color: { type: string, pattern: '^#[0-9A-Fa-f]{6}$', default: '#7355d9' }
    CategoryUpdate:
      type: object
      required: [expected_revision]
      additionalProperties: false
      properties:
        expected_revision: { type: integer, minimum: 1 }
        name: { type: string, minLength: 2, maxLength: 80 }
        color: { type: string, pattern: '^#[0-9A-Fa-f]{6}$' }
    CategoryPage:
      allOf:
        - $ref: '#/components/schemas/PageBase'
        - type: object
          properties:
            data:
              type: array
              items: { $ref: '#/components/schemas/Category' }
    Price:
      type: object
      required: [currency_code, sell_price_minor, tax_rate_bps, includes_tax, version]
      properties:
        currency_code: { type: string, pattern: '^[A-Z]{3}$' }
        sell_price_minor: { type: integer, minimum: 0 }
        tax_rate_bps: { type: integer, minimum: 0, maximum: 10000 }
        includes_tax: { type: boolean }
        version: { type: integer, minimum: 0 }
    Stock:
      type: object
      required: [location_id, quantity_thousandths]
      properties:
        location_id: { type: string, format: uuid }
        quantity_thousandths: { type: integer }
    Product:
      type: object
      required: [id, name, category, unit, revision, state, price, updated_at]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        name_ar: { type: [string, 'null'] }
        category:
          type: object
          required: [id, name]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
        description: { type: string }
        brand: { type: string }
        unit: { type: string, enum: [PIECE, KG, LITRE, BAG, BOX, METER, ROLL, PACK] }
        size: { type: string }
        sku: { type: [string, 'null'] }
        barcode: { type: [string, 'null'] }
        batch_tracked: { type: boolean }
        restricted: { type: boolean }
        sellable_on_pos: { type: boolean }
        publish_to_web: { type: boolean }
        low_stock_at_thousandths: { type: integer, minimum: 0 }
        state: { type: string, enum: [active, archived] }
        revision: { type: integer, minimum: 1 }
        price: { $ref: '#/components/schemas/Price' }
        stock: { $ref: '#/components/schemas/Stock' }
        updated_at: { type: string, format: date-time }
    ProductCreate:
      type: object
      required: [id, category_id, name, unit, sell_price_minor]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        category_id: { type: string, format: uuid }
        name: { type: string, minLength: 2, maxLength: 140 }
        name_ar: { type: [string, 'null'], minLength: 2, maxLength: 140 }
        description: { type: string, maxLength: 2000 }
        brand: { type: string, maxLength: 100 }
        unit: { type: string, enum: [PIECE, KG, LITRE, BAG, BOX, METER, ROLL, PACK] }
        size: { type: string, maxLength: 80 }
        sku: { type: [string, 'null'], maxLength: 64 }
        barcode: { type: [string, 'null'], maxLength: 64 }
        sell_price_minor: { type: integer, minimum: 0 }
        tax_rate_bps: { type: integer, minimum: 0, maximum: 10000, default: 0 }
        price_includes_tax: { type: boolean, default: true }
        low_stock_at_thousandths: { type: integer, minimum: 0, default: 0 }
        batch_tracked: { type: boolean, default: false }
        restricted: { type: boolean, default: false }
        sellable_on_pos: { type: boolean, default: true }
        publish_to_web: { type: boolean, default: false }
    ProductUpdate:
      type: object
      required: [expected_revision]
      additionalProperties: false
      properties:
        expected_revision: { type: integer, minimum: 1 }
        category_id: { type: string, format: uuid }
        name: { type: string, minLength: 2, maxLength: 140 }
        name_ar: { type: [string, 'null'], minLength: 2, maxLength: 140 }
        description: { type: string, maxLength: 2000 }
        brand: { type: string, maxLength: 100 }
        unit: { type: string, enum: [PIECE, KG, LITRE, BAG, BOX, METER, ROLL, PACK] }
        size: { type: string, maxLength: 80 }
        sku: { type: [string, 'null'], maxLength: 64 }
        barcode: { type: [string, 'null'], maxLength: 64 }
        sell_price_minor: { type: integer, minimum: 0 }
        tax_rate_bps: { type: integer, minimum: 0, maximum: 10000 }
        price_includes_tax: { type: boolean }
        low_stock_at_thousandths: { type: integer, minimum: 0 }
        batch_tracked: { type: boolean }
        restricted: { type: boolean }
        sellable_on_pos: { type: boolean }
        publish_to_web: { type: boolean }
    ProductPage:
      allOf:
        - $ref: '#/components/schemas/PageBase'
        - type: object
          properties:
            data:
              type: array
              items: { $ref: '#/components/schemas/Product' }
    InventoryPosition:
      type: object
      required: [location_id, product, quantity_thousandths, low_stock_at_thousandths, revision]
      properties:
        location_id: { type: string, format: uuid }
        product:
          type: object
          required: [id, name, unit]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            sku: { type: [string, 'null'] }
            unit: { type: string }
        quantity_thousandths: { type: integer }
        low_stock_at_thousandths: { type: integer, minimum: 0 }
        revision: { type: integer, minimum: 0 }
    InventoryPage:
      allOf:
        - $ref: '#/components/schemas/PageBase'
        - type: object
          properties:
            data:
              type: array
              items: { $ref: '#/components/schemas/InventoryPosition' }
    InventoryAdjustment:
      type: object
      additionalProperties: false
      required: [product_id, expected_revision, counted_quantity_thousandths, reason]
      properties:
        product_id: { type: string, format: uuid }
        expected_revision: { type: integer, minimum: 0 }
        counted_quantity_thousandths: { type: integer, minimum: 0 }
        reason: { type: string, minLength: 3, maxLength: 300 }
    Event:
      type: object
      required: [id, type, version, aggregate_type, aggregate_id, occurred_at, data]
      properties:
        id: { type: string, format: uuid }
        type: { type: string }
        version: { type: integer, minimum: 1 }
        aggregate_type: { type: string, enum: [catalog_product, catalog_category, inventory_product] }
        aggregate_id: { type: string }
        occurred_at: { type: string, format: date-time }
        data: { type: object, additionalProperties: true }
    EventPage:
      type: object
      required: [data, next_cursor, has_more]
      properties:
        data:
          type: array
          items: { $ref: '#/components/schemas/Event' }
        next_cursor: { type: [string, 'null'] }
        has_more: { type: boolean }
    Error:
      type: object
      required: [error, request_id]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            fields:
              type: object
              additionalProperties: { type: string }
        request_id: { type: string, format: uuid }
