openapi: 3.1.0
info:
  title: POSnavigator Hero Entry Analytics API
  version: "1.0.0"
  description: |-
    Admin-only aggregates from hero_entry_analytics_events — the
    homepage hero / preset funnel stored in Mongo, not a GA4 export.
    
    Requires X-Api-Key, hero-analytics:read, and an admin key owner. Query from/to
    as YYYY-MM-DD (UTC days, to inclusive), default last 7 days, max 90 days.
    locale hu|en localizes entity names. limit 1-2000 (default 500) truncates only
    the rows breakdown; uniqueSessions, totals, funnel, sessionFunnel, dropOffs
    always cover the full period. rowsTruncated true means the long tail was dropped.
    
    # The happy path
    
    1. GET /openapi/hero-entry-analytics.yaml
    2. GET /api/v1/hero-entry/analytics?from=2026-07-01&to=2026-07-31&locale=hu
    
    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_QUERY 400.
servers:
  - url: https://posnavigator.eu
tags:
  - name: Discovery
  - name: Analytics
security:
  - ApiKeyAuth: []
paths:
  /openapi/hero-entry-analytics.yaml:
    get:
      operationId: getHeroEntryAnalyticsOpenApi
      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/hero-entry/analytics:
    get:
      operationId: getHeroEntryAnalytics
      tags: [Analytics]
      summary: Hero / preset-entry funnel aggregates
      description: |-
        Returns uniqueSessions, totalsByEventType, funnel counts, sessionFunnel,
        dropOffs, and optional rows. Event types: hero_entry_impression,
        hero_channel_click, hero_service_type_click, hero_preset_card_click,
        hero_modal_opened, hero_modal_approved, hero_handoff_succeeded,
        hero_handoff_failed, hero_route_empty_runtime, hero_selection_expired,
        hero_offer_list_loaded, hero_offer_filters_applied, hero_offer_card_opened,
        hero_offer_added_to_basket, hero_order_summary_reached,
        hero_order_initiated, hero_order_completed. Admin + hero-analytics:read
        required. Limit does not shrink funnel totals.
      parameters:
        - name: from
          in: query
          schema: { type: string, pattern: '^\d{4}-\d{2}-\d{2}$', maxLength: 10 }
          description: UTC start day, default 6 days before today
        - name: to
          in: query
          schema: { type: string, pattern: '^\d{4}-\d{2}-\d{2}$', maxLength: 10 }
          description: UTC inclusive end day, default today
        - name: locale
          in: query
          schema: { type: string, enum: [hu, en], default: hu }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 2000, default: 500 }
          description: Max rows breakdown lines only
      responses:
        '200':
          description: Aggregates
          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/HeroAnalyticsResponse'
              example:
                success: true
                data:
                  uniqueSessions: 128
                  rowsTruncated: false
        '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'
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
    HeroAnalyticsResponse:
      type: object
      required: [success, data]
      additionalProperties: false
      properties:
        success: { type: boolean, const: true }
        data:
          type: object
          additionalProperties: true
          properties:
            periodStart: { type: string, maxLength: 40 }
            periodEnd: { type: string, maxLength: 40 }
            uniqueSessions: { type: integer }
            totalsByEventType:
              type: object
              additionalProperties: { type: integer }
            funnel:
              type: object
              additionalProperties: { type: integer }
            sessionFunnel:
              type: array
              maxItems: 40
              items: { type: object, additionalProperties: true }
            dropOffs:
              type: array
              maxItems: 40
              items: { type: object, additionalProperties: true }
            rowLimit: { type: integer }
            rowsTruncated: { type: boolean }
            rows:
              type: array
              maxItems: 2000
              items: { type: object, additionalProperties: true }

