openapi: 3.1.0
info:
  title: POSnavigator Callback Requests API
  version: "1.0.0"
  description: |-
    Admin-only v1 API for the concierge callback queue shown in the admin
    "Visszahívás!" screen. Each row is a WizardLead with callback_requested_at set
    (homepage concierge or offer-list callback). Agents can list, read, change
    callback_status, and append internal comments — the same operations the admin UI
    exposes for status and notes.
    
    # Access
    
    X-Api-Key and an admin key owner. No extra permission scope is required — any
    valid pn_ key whose owner is admin works, including keys created before
    callback-request:read/write scopes existed. Non-admin keys get 403 FORBIDDEN.
    New keys may list callback-request scopes for documentation; they are optional.
    
    callback_status values: pending, no_answer, contacted, offer_sent, sent_to_bank,
    closed. callback_source: homepage (hero concierge) or offer_list (wizard CTA).
    
    # The happy path
    
    1. GET /openapi/callback-requests.yaml
    2. GET /api/v1/callback-requests?callback_status=pending&limit=50
    3. GET /api/v1/callback-requests/{leadId}
    4. PATCH { "callback_status": "contacted" }
    5. POST /comments { "text": "Holnap 10:00 visszahívás" }
    
    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. Missing optional scopes on older keys do not
    block this API. Non-admin key owners get 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; admin key required |
    | 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: USER_NOT_FOUND 401, INVALID_QUERY 400, INVALID_LEAD_ID 400,
    INVALID_JSON 400, INVALID_PAYLOAD 400, NOT_FOUND 404.
servers:
  - url: https://posnavigator.eu
tags:
  - name: Discovery
  - name: CallbackRequests
security:
  - ApiKeyAuth: []
paths:
  /openapi/callback-requests.yaml:
    get:
      operationId: getCallbackRequestsOpenApi
      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/callback-requests:
    get:
      operationId: listCallbackRequests
      tags: [CallbackRequests]
      summary: List callback requests
      description: |-
        Admin-only paginated list of concierge callback leads (callback_requested_at
        is set). Sorted by callback_requested_at descending. Filter by callback_status,
        callback_source, email substring, draft_status, delivery_status, or
        marketing_opt_in (1|true|0|false). limit 1-100 default 50, offset default 0.
        Empty items is success, not 404.
      parameters:
        - name: callback_status
          in: query
          schema:
            type: string
            enum: [pending, no_answer, contacted, offer_sent, sent_to_bank, closed]
        - name: callback_source
          in: query
          schema:
            type: string
            enum: [homepage, offer_list]
        - name: email
          in: query
          schema: { type: string, maxLength: 255 }
          description: Case-insensitive substring match on lead email
        - name: draft_status
          in: query
          schema: { type: string, enum: [active, completed, expired] }
        - name: delivery_status
          in: query
          schema: { type: string, enum: [unknown, delivered, bounced, complained] }
        - name: marketing_opt_in
          in: query
          schema: { type: string, enum: ['1', 'true', '0', 'false'] }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: Callback request page
          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/CallbackRequestListResponse'
        '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'
  /api/v1/callback-requests/{leadId}:
    parameters:
      - name: leadId
        in: path
        required: true
        description: 24-character hex Mongo ObjectId of the WizardLead
        schema:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
          maxLength: 24
    get:
      operationId: getCallbackRequest
      tags: [CallbackRequests]
      summary: Get one callback request
      description: |-
        Admin-only. Returns the normalized callback lead with linked_orders and the
        full callback_comments history. Unknown leadId or a lead without
        callback_requested_at is 404 NOT_FOUND.
      responses:
        '200':
          description: Callback request
          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/CallbackRequestResponse'
        '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: updateCallbackRequestStatus
      tags: [CallbackRequests]
      summary: Update callback_status
      description: |-
        Admin-only. Sets callback_status and bumps last_activity_at. Body must
        include callback_status with one of the enum values. Returns the updated
        lead. Unknown leadId or a lead without callback_requested_at is 404.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackRequestStatusPatch'
            example:
              callback_status: contacted
      responses:
        '200':
          description: Updated callback request
          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/CallbackRequestResponse'
        '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'
  /api/v1/callback-requests/{leadId}/comments:
    parameters:
      - name: leadId
        in: path
        required: true
        description: 24-character hex Mongo ObjectId of the WizardLead
        schema:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
          maxLength: 24
    post:
      operationId: addCallbackRequestComment
      tags: [CallbackRequests]
      summary: Append an internal comment
      description: |-
        Admin-only. Appends a comment to callback_comments and bumps last_activity_at.
        Comments are never edited or deleted via this API. text is 1-2000 characters.
        Response includes the saved comment and the full updated lead.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallbackRequestCommentCreate'
            example:
              text: Holnap 10:00 visszahívás
      responses:
        '200':
          description: Comment saved
          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/CallbackRequestCommentResponse'
        '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'
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
  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
    OffsetPagination:
      type: object
      additionalProperties: false
      required: [total, limit, offset, hasMore]
      properties:
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
        hasMore:
          type: boolean
    CallbackRequestComment:
      type: object
      additionalProperties: false
      required: [at, admin_user_id, admin_name, text]
      properties:
        at:
          type: string
          maxLength: 40
        admin_user_id:
          type: string
          maxLength: 24
        admin_name:
          type: string
          maxLength: 200
        text:
          type: string
          maxLength: 2000
    LinkedOrder:
      type: object
      additionalProperties: false
      required: [_id]
      properties:
        _id:
          type: string
          maxLength: 24
        createdAt:
          type: string
          maxLength: 40
    CallbackRequest:
      type: object
      additionalProperties: false
      required:
        - _id
        - email
        - email_locale
        - marketing_opt_in_requested
        - draft_status
        - last_activity_at
        - delivery_status
        - callback_requested_at
      properties:
        _id:
          type: string
          maxLength: 24
        email:
          type: string
          maxLength: 255
        email_locale:
          type: string
          enum: [hu, en]
        marketing_opt_in_requested:
          type: boolean
        draft_status:
          type: string
          enum: [active, completed, expired]
        last_activity_at:
          type: string
          maxLength: 40
        last_completed_step:
          type: [integer, null]
        subscriber_id:
          type: [string, null]
          maxLength: 24
        completed_at:
          type: [string, null]
          maxLength: 40
        delivery_status:
          type: string
          enum: [unknown, delivered, bounced, complained]
        latest_offer_type_key:
          type: [string, null]
          maxLength: 200
        callback_requested_at:
          type: [string, null]
          maxLength: 40
        callback_contact_name:
          type: [string, null]
          maxLength: 200
        callback_phone:
          type: [string, null]
          maxLength: 40
        callback_status:
          type: [string, null]
          enum: [pending, no_answer, contacted, offer_sent, sent_to_bank, closed, null]
        callback_company_name:
          type: [string, null]
          maxLength: 200
        callback_tax_number:
          type: [string, null]
          maxLength: 40
        callback_cegjelzo_id:
          type: [string, null]
          maxLength: 80
        callback_source:
          type: [string, null]
          enum: [homepage, offer_list, null]
        callback_consent_at:
          type: [string, null]
          maxLength: 40
        callback_provider_share_consent_at:
          type: [string, null]
          maxLength: 40
        callback_filters_query:
          type: [string, null]
          maxLength: 8000
        callback_comments:
          type: array
          maxItems: 500
          items:
            $ref: '#/components/schemas/CallbackRequestComment'
        linked_orders:
          type: array
          maxItems: 50
          items:
            $ref: '#/components/schemas/LinkedOrder'
        createdAt:
          type: string
          maxLength: 40
        updatedAt:
          type: string
          maxLength: 40
    CallbackRequestListResponse:
      type: object
      additionalProperties: false
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          additionalProperties: false
          required: [items, pagination]
          properties:
            items:
              type: array
              maxItems: 100
              items:
                $ref: '#/components/schemas/CallbackRequest'
            pagination:
              $ref: '#/components/schemas/OffsetPagination'
    CallbackRequestResponse:
      type: object
      additionalProperties: false
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: '#/components/schemas/CallbackRequest'
    CallbackRequestStatusPatch:
      type: object
      additionalProperties: false
      required: [callback_status]
      properties:
        callback_status:
          type: string
          enum: [pending, no_answer, contacted, offer_sent, sent_to_bank, closed]
    CallbackRequestCommentCreate:
      type: object
      additionalProperties: false
      required: [text]
      properties:
        text:
          type: string
          minLength: 1
          maxLength: 2000
    CallbackRequestCommentResponse:
      type: object
      additionalProperties: false
      required: [success, data]
      properties:
        success:
          type: boolean
          const: true
        data:
          type: object
          additionalProperties: false
          required: [comment, item]
          properties:
            comment:
              $ref: '#/components/schemas/CallbackRequestComment'
            item:
              $ref: '#/components/schemas/CallbackRequest'
