openapi: 3.1.0
info:
  title: POSnavigator Product Object API
  version: "1.0.0"
  description: |-
    Canonical contract for product records and physical-device sales
    configurations. An agent with a valid API key can list a bank's products, read
    one product, and — with an admin key — create, patch, delete, or replace device
    configurations.
    
    The bank field is derived from mainservice; do not invent it. Nested feesets
    and fees are not written here — propose fee changes through provider-research
    change proposals, or use the admin UI.
    
    # Access
    
    Any valid key can GET products whose bank is in the user's banks array (admins
    see all). POST, PATCH, DELETE, and PUT device-configurations are admin-only
    (403 FORBIDDEN otherwise). Admin-only fields on PATCH include translations.*.evaluation,
    urls, and isadvertised.
    
    # The happy path
    
    1. GET /openapi/products.yaml
    2. GET /api/v1/products?bankId={bankId}
    3. GET /api/v1/products/{productId} before a patch
    4. PATCH translations or flags; PUT device-configurations as a full devices array
    
    Send `X-Api-Key: <key>` on every operation except this document itself.
    Create a key at /user/api-keys. Format: `pn_` plus a random string. Copy it
    immediately; it is shown once. Default new keys include blog, legal, presetfilter,
    versus-landing, widget, agent-analytics and hero-analytics scopes. Missing scopes
    return 403 INSUFFICIENT_PERMISSIONS. Bank-scoped resources also require the key
    owner to be admin or listed on that bank; otherwise 403 FORBIDDEN.
    
    # Response envelope
    
    Success: `{ "success": true, "data": ... }`.
    Failure: `{ "success": false, "error": { "code", "message" } }`.
    Branch on error.code, never on error.message.
    
    # Error codes
    
    | code | status | what to do |
    | --- | --- | --- |
    | MISSING_API_KEY | 401 | add X-Api-Key; do not retry blindly |
    | INVALID_API_KEY | 401 | stop; ask a human for a new key |
    | INSUFFICIENT_PERMISSIONS | 403 | stop; this key lacks a required scope |
    | FORBIDDEN | 403 | stop or switch to a key with bank/admin access |
    | RATE_LIMIT_EXCEEDED | 429 | wait Retry-After seconds |
    | INTERNAL_ERROR | 500 | retry with backoff |
    
    # Rate limiting
    
    Every response after successful authentication carries X-RateLimit-Limit,
    X-RateLimit-Remaining and X-RateLimit-Reset. A 429 also carries Retry-After.
    The two 401s are emitted before rate-limit accounting. Default window is 100
    requests per rolling hour unless the key was configured otherwise.
    
    # Canonical host
    
    Use https://posnavigator.eu. Legacy .hu hosts redirect; authenticated writes
    should call .eu directly.
    
    Additional codes: INVALID_PRODUCT_ID 400, MISSING_BANK_ID 400, INVALID_PAYLOAD 400,
    PRODUCT_NOT_FOUND 404, PRODUCT_HAS_FEESETS 409, PRODUCT_HAS_OPTIONSETS 409.
servers:
  - url: https://posnavigator.eu
tags:
  - name: Discovery
  - name: Products
  - name: DeviceConfigurations
security:
  - ApiKeyAuth: []
paths:
  /openapi/products.yaml:
    get:
      operationId: getProductsOpenApi
      tags: [Discovery]
      security: []
      summary: Public OpenAPI 3.1 description
      description: |-
        This document. Public: no API key, no rate limit. Fetch it first and treat
        it as the complete manual. Every schema, limit, example, error code and
        header an agent needs is here. Linked from GET /openapi/catalog.yaml.
        Data operations below require authentication as described in info.
      responses:
        '200':
          description: OpenAPI YAML
          content:
            application/yaml:
              schema:
                type: string
              example: 'openapi: 3.1.0'
  /api/v1/products:
    get:
      operationId: listProducts
      tags: [Products]
      summary: List products for a bank
      description: |-
        Requires query bankId (24-hex ObjectId). Returns every product whose
        derived bank matches, including translations and flags. Use this to
        discover productId values. Non-admin keys may only list a bank they
        belong to. Side-effect free. Not paginated.
      parameters:
        - name: bankId
          in: query
          required: true
          description: Bank ObjectId whose products to list
          schema:
            type: string
            pattern: '^[a-fA-F0-9]{24}$'
            maxLength: 24
      responses:
        '200':
          description: Product list
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductListResponse'
              example:
                success: true
                data:
                  - _id: '507f1f77bcf86cd799439099'
                    mainservice: '507f1f77bcf86cd799439088'
                    isprod: true
        '400':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    post:
      operationId: createProduct
      tags: [Products]
      summary: Create a product (admin only)
      description: |-
        Admin-only. Required body field: mainservice (existing ObjectId). The
        server derives bank from that mainservice. New products should be
        created as draft/non-prod unless you intend to publish immediately.
        Returns the created product id in data.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductCreateRequest'
            example:
              mainservice: '507f1f77bcf86cd799439088'
              translations:
                hu: { name: Példa POS }
                en: { name: Example POS }
      responses:
        '201':
          description: Created
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductWriteResponse'
        '400':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/v1/products/{productId}:
    parameters:
      - name: productId
        in: path
        required: true
        schema:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
          maxLength: 24
    get:
      operationId: getProduct
      tags: [Products]
      summary: Get one product
      description: |-
        Returns the product object including translations, income/basket bands,
        flags, and media. Bank-scoped keys may only read products on banks they
        can access. Use this payload as the base for a PATCH merge of
        translations.{hu,en}.
      responses:
        '200':
          description: Product
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductDetailResponse'
        '400':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    patch:
      operationId: patchProduct
      tags: [Products]
      summary: Partially update a product (admin only)
      description: |-
        Admin-only partial update. translations.hu and translations.en merge with
        stored locale objects. Changing mainservice re-derives bank. Do not send
        nested feesets here. Admin-only fields (evaluation, urls, isadvertised)
        reject non-admin keys. Validity windows accept ISO dates or null.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductPatchRequest'
      responses:
        '200':
          description: Updated product
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductDetailResponse'
        '400':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    delete:
      operationId: deleteProduct
      tags: [Products]
      summary: Delete a product (admin only)
      description: |-
        Admin-only hard delete. Refuses with 409 PRODUCT_HAS_FEESETS or
        PRODUCT_HAS_OPTIONSETS when related records still exist. Delete or move
        those first. Irreversible.
      responses:
        '200':
          description: Deleted
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkResponse'
        '400':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
  /api/v1/products/{productId}/device-configurations:
    parameters:
      - name: productId
        in: path
        required: true
        schema:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
          maxLength: 24
    get:
      operationId: getProductDeviceConfigurations
      tags: [DeviceConfigurations]
      summary: Read device sales configurations
      description: |-
        Returns physical_device_sales.device_configurations for the product:
        catalog item id, VAT, active flag, display order, and localized display
        name/description. Readable by keys that can read the product.
      responses:
        '200':
          description: Device configuration list
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceConfigurationsResponse'
        '400':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    put:
      operationId: putProductDeviceConfigurations
      tags: [DeviceConfigurations]
      summary: Replace device sales configurations (admin only)
      description: |-
        Admin-only. Replaces the entire devices array with the body you send.
        GET first, edit the array, PUT the full list. Each item needs
        device_catalog_item_id, vat_rate, is_active, display_order, and
        display_name / display_description locale objects.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceConfigurationsPutRequest'
            example:
              devices:
                - device_catalog_item_id: '507f1f77bcf86cd799439011'
                  vat_rate: 27
                  is_active: true
                  display_order: 0
                  display_name: { hu: Mobil POS, en: Mobile POS }
                  display_description: { hu: Leírás, en: Description }
      responses:
        '200':
          description: Replaced configurations
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceConfigurationsResponse'
        '400':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
            Retry-After:
              $ref: '#/components/headers/Retry-After'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          description: Error
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: |-
        Send `X-Api-Key: pn_...` on every data operation. Create a key at
        /user/api-keys; it is shown once. Inactive or expired keys return 401.
  headers:
    X-RateLimit-Limit:
      description: Maximum requests allowed in the current window
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Requests remaining in the current window
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Unix epoch seconds when the window resets
      schema:
        type: integer
    Retry-After:
      description: Seconds to wait before retrying a 429
      schema:
        type: integer
  parameters:
    BankId:
      name: bankId
      in: path
      required: true
      description: 24-character hex Mongo ObjectId of the bank
      schema:
        type: string
        pattern: '^[a-fA-F0-9]{24}$'
        maxLength: 24

  schemas:
    ErrorEnvelope:
      type: object
      additionalProperties: false
      required: [success, error]
      properties:
        success:
          type: boolean
          const: false
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              type: string
              maxLength: 80
              description: Stable machine code. Branch on this, never on message.
            message:
              type: string
              maxLength: 2000
            details:
              type: array
              maxItems: 50
              items:
                type: string
                maxLength: 500
            retryAfter:
              type: integer
              description: Present on RATE_LIMIT_EXCEEDED
            field_errors:
              type: object
              additionalProperties:
                type: array
                maxItems: 20
                items:
                  type: string
                  maxLength: 500
    LocalePair:
      type: object
      additionalProperties: false
      properties:
        hu:
          type: string
          maxLength: 20000
        en:
          type: string
          maxLength: 20000
    OffsetPagination:
      type: object
      additionalProperties: false
      required: [total, limit, offset, hasMore]
      properties:
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
        hasMore:
          type: boolean
    OkResponse:
      type: object
      required: [success]
      additionalProperties: true
      properties:
        success:
          type: boolean
          const: true
    ProductListResponse:
      type: object
      required: [success, data]
      additionalProperties: false
      properties:
        success: { type: boolean, const: true }
        data:
          type: array
          maxItems: 2000
          items:
            type: object
            additionalProperties: true
    ProductDetailResponse:
      type: object
      required: [success, data]
      additionalProperties: false
      properties:
        success: { type: boolean, const: true }
        data:
          type: object
          additionalProperties: true
    ProductWriteResponse:
      type: object
      required: [success]
      additionalProperties: true
      properties:
        success: { type: boolean, const: true }
        data:
          type: object
          additionalProperties: true
    ProductCreateRequest:
      type: object
      required: [mainservice]
      additionalProperties: true
      properties:
        mainservice:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
          maxLength: 24
          description: Existing mainservice ObjectId; bank is derived from it
        translations:
          type: object
          additionalProperties: true
          description: hu/en name, description, marketing copy, pricing, etc.
    ProductPatchRequest:
      type: object
      additionalProperties: true
      description: Partial product update. Locale objects merge.
      properties:
        translations:
          type: object
          additionalProperties: true
          description: Merge per locale. evaluation, urls, isadvertised are admin-only.
        mainservice:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
          maxLength: 24
          description: Changing this re-derives bank
        incomefrom: { type: number }
        incometo: { type: number }
        basketfrom: { type: number }
        basketto: { type: number }
        isprod: { type: boolean }
        isorderable: { type: boolean }
        isposdevice: { type: boolean }
        iswebstore: { type: boolean }
        iscard: { type: boolean }
        isaccountrequired: { type: boolean }
        isuniquepricing: { type: boolean }
        isadvertised:
          type: boolean
          description: Admin only
        priority: { type: integer }
        validfrom:
          type: [string, 'null']
          maxLength: 40
        validto:
          type: [string, 'null']
          maxLength: 40
        promotions:
          type: array
          maxItems: 50
          items: { type: string, maxLength: 24 }
        urls:
          type: array
          maxItems: 50
          items: { type: string, maxLength: 2000 }
          description: Admin-only AI analysis source URLs
    DeviceConfigItem:
      type: object
      additionalProperties: true
      required: [device_catalog_item_id, vat_rate, is_active, display_order]
      properties:
        device_catalog_item_id:
          type: string
          maxLength: 24
          description: Device catalog ObjectId
        vat_rate:
          type: number
          description: VAT percent, typically 27 in HU
        is_active: { type: boolean }
        display_order: { type: integer }
        display_name:
          $ref: '#/components/schemas/LocalePair'
        display_description:
          $ref: '#/components/schemas/LocalePair'
    DeviceConfigurationsResponse:
      type: object
      required: [success, data]
      additionalProperties: true
      properties:
        success: { type: boolean, const: true }
        data:
          type: object
          additionalProperties: true
          properties:
            devices:
              type: array
              maxItems: 100
              items:
                $ref: '#/components/schemas/DeviceConfigItem'
    DeviceConfigurationsPutRequest:
      type: object
      required: [devices]
      additionalProperties: false
      properties:
        devices:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/DeviceConfigItem'
          description: Full replacement list, not a patch

