openapi: 3.1.0
info:
  title: POSnavigator Legal Documents API
  version: "1.0.0"
  description: |-
    ÁSZF (GTC) and privacy-policy versioning. GET /api/gtc and
    GET /api/privacy are public — no API key. POST /api/legal/update creates a new
    immutable version and requires X-Api-Key plus legal:write.
    
    Current version = isprod true AND from_date <= today, latest from_date wins.
    Optional ?version=N selects a specific version_number. Documents are never
    deleted. Public GET responses are not the v1 {success,data} envelope; they are
    {title, content, from_date, to_date, version_number}. The upload POST returns
    {success, message, id} on success and a flat {error} object on many failures.
    
    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.
    
    Upload-specific: 409 when version_number already exists for that type. Zod
    failures are 400 with details. Reads: 400 missing/invalid locale, 404 no document.
servers:
  - url: https://posnavigator.eu
tags:
  - name: Discovery
  - name: Legal
security:
  - ApiKeyAuth: []
paths:
  /openapi/legal-documents.yaml:
    get:
      operationId: getLegalDocumentsOpenApi
      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/gtc:
    get:
      operationId: getGtc
      tags: [Legal]
      security: []
      summary: Public current or archived GTC
      description: |-
        Public. Required query locale=hu|en. Optional version selects
        version_number. Without version, returns the current production document
        whose from_date is on or before today. content is HTML (stored as body).
      parameters:
        - name: locale
          in: query
          required: true
          schema: { type: string, enum: [hu, en] }
        - name: version
          in: query
          schema: { type: integer, minimum: 1 }
      responses:
        '200':
          description: GTC HTML
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegalPublicDocument'
              example:
                title: Általános Szerződési Feltételek
                content: <h1>ÁSZF</h1>
                from_date: '2026-01-01T00:00:00.000Z'
                to_date: null
                version_number: 3
        '400':
          description: Missing or invalid locale
        '404':
          description: No GTC found
        '500':
          description: Server error
  /api/privacy:
    get:
      operationId: getPrivacy
      tags: [Legal]
      security: []
      summary: Public current or archived privacy policy
      description: |-
        Public. Same query rules as getGtc: locale required, version optional.
        Returns the current production privacy document unless a version is named.
      parameters:
        - name: locale
          in: query
          required: true
          schema: { type: string, enum: [hu, en] }
        - name: version
          in: query
          schema: { type: integer, minimum: 1 }
      responses:
        '200':
          description: Privacy HTML
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LegalPublicDocument'
        '400':
          description: Missing or invalid locale
        '404':
          description: No privacy policy found
        '500':
          description: Server error
  /api/legal/update:
    post:
      operationId: createLegalVersion
      tags: [Legal]
      summary: Upload a new GTC or privacy version
      description: |-
        Requires X-Api-Key and legal:write. Creates a new isprod document; does
        not delete older versions. type is gtc or privacy. from_date is YYYY-MM-DD
        (optional time). version_number must be a new positive integer for that
        type (409 if taken). translations.hu/en each need title and body HTML.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LegalUploadRequest'
            example:
              type: gtc
              from_date: '2026-09-01'
              version_number: 4
              translations:
                hu: { title: ÁSZF, body: <h1>ÁSZF</h1><p>Hatályos.</p> }
                en: { title: GTC, body: <h1>GTC</h1><p>Effective.</p> }
      responses:
        '200':
          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/LegalUploadResponse'
              example:
                success: true
                message: GTC version 4 created
                id: '6684f1a2c3d4e5f6a7b8c9d0'
        '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'
        '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'
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
    LegalPublicDocument:
      type: object
      additionalProperties: false
      required: [title, content, version_number]
      properties:
        title: { type: string, maxLength: 300 }
        content: { type: string, maxLength: 500000, description: HTML body }
        from_date: { type: string, maxLength: 40 }
        to_date:
          type: [string, 'null']
          maxLength: 40
        version_number: { type: integer }
    LegalUploadRequest:
      type: object
      required: [type, from_date, version_number, translations]
      additionalProperties: false
      properties:
        type:
          type: string
          enum: [gtc, privacy]
          description: gtc is ÁSZF; privacy is adatkezelési tájékoztató
        from_date:
          type: string
          maxLength: 40
          description: YYYY-MM-DD or ISO datetime; effective date
        version_number:
          type: integer
          minimum: 1
          description: Must be unused for this type
        translations:
          type: object
          required: [hu, en]
          additionalProperties: false
          properties:
            hu:
              type: object
              required: [title, body]
              additionalProperties: false
              properties:
                title: { type: string, maxLength: 300 }
                body: { type: string, maxLength: 500000 }
            en:
              type: object
              required: [title, body]
              additionalProperties: false
              properties:
                title: { type: string, maxLength: 300 }
                body: { type: string, maxLength: 500000 }
    LegalUploadResponse:
      type: object
      additionalProperties: true
      properties:
        success: { type: boolean }
        message: { type: string, maxLength: 300 }
        id: { type: string, maxLength: 24 }

