# Frontpagely Public REST API: OpenAPI 3.1 spec (source of truth).
#
# Scope: this file is the canonical contract for /v1. Handlers in
# functions/v1/ MUST match it; build/lib/api-drift-guard.ts checks both
# directions at build time (no handler without a spec entry, no spec
# entry without a handler).
#
# Phase-0 fork note: this is a MINIMAL spec covering only the surviving
# v1 surface: tracked entities (the renamed properties resource) and
# webhook endpoints (platform tables, inherited as-is). The PreArrive
# packet / reservation / certificate / sync-source / SMS resources are
# dropped along with their handlers. Scopes are entities:read|write and
# webhooks:read|write (the properties:* / packets:* / reservations:* /
# certificates:* / sync_sources:* / sms_messages:* scopes are retired).
# Real per-vertical run/report resources land in Phase 1.
#
# Editor: any YAML editor works; for live validation use
# https://editor.swagger.io/ or `npx @stoplight/spectral-cli lint`.

openapi: "3.1.0"

info:
  title: Frontpagely REST API
  version: "1.0.0"
  summary: Server-to-server REST API for Frontpagely (Pro plan).
  description: |
    The Frontpagely REST API lets Pro-tier customers integrate
    AI-visibility tracking into their own dashboards and automation:
    manage the brands you track (tracked entities) and subscribe to
    lifecycle events via webhooks.

    **Authentication** is by API key (bearer token). Issue keys at
    `/app/settings/api`. Keys are team-scoped: they grant the same
    access the team owner has, optionally narrowed by scopes
    (`entities:read`, `entities:write`, `webhooks:read`, `webhooks:write`).

    **Versioning** is stable for v1. Breaking changes ship in v2;
    v1 continues to serve for at least 18 months after v2 lands.
  contact:
    name: Frontpagely Support
    url: https://frontpagely.com/contact/
  license:
    name: Proprietary

servers:
  - url: https://api.frontpagely.com/v1
    description: Production (live keys + live data)

security:
  - bearerAuth: []

tags:
  - name: tracked_entities
    description: The brands, firms, or sites whose AI visibility you track (one row per entity).
  - name: webhook_endpoints
    description: Outbound webhooks for lifecycle events.
  - name: runs
    description: Measurement runs, one measurement period for a tracked entity (methodology §9a).
  - name: visibility
    description: Visibility snapshots, the aggregated SeenRate trend record for a tracked entity (methodology §1.4 / §4.5).

# ---------------------------------------------------------------- paths
paths:
  /tracked_entities:
    get:
      tags: [tracked_entities]
      summary: List tracked entities
      description: Returns a paginated list of the team's tracked entities. Requires the `entities:read` scope.
      operationId: listTrackedEntities
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/EndingBefore"
      responses:
        "200":
          description: A page of tracked entities.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, enum: [list] }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/TrackedEntity" }
                  has_more: { type: boolean }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [tracked_entities]
      summary: Create a tracked entity
      description: Creates a new tracked entity. Requires the `entities:write` scope.
      operationId: createTrackedEntity
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TrackedEntityCreate" }
      responses:
        "201":
          description: The created tracked entity.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TrackedEntity" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /tracked_entities/{id}:
    parameters:
      - $ref: "#/components/parameters/EntityId"
    get:
      tags: [tracked_entities]
      summary: Retrieve a tracked entity
      description: Returns a single tracked entity by id. Requires the `entities:read` scope.
      operationId: getTrackedEntity
      responses:
        "200":
          description: The tracked entity.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TrackedEntity" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [tracked_entities]
      summary: Update a tracked entity
      description: Updates mutable fields on a tracked entity. Requires the `entities:write` scope.
      operationId: updateTrackedEntity
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TrackedEntityCreate" }
      responses:
        "200":
          description: The updated tracked entity.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TrackedEntity" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [tracked_entities]
      summary: Delete a tracked entity
      description: Deletes a tracked entity. Requires the `entities:write` scope.
      operationId: deleteTrackedEntity
      responses:
        "204":
          description: Deleted. No content.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhook_endpoints:
    get:
      tags: [webhook_endpoints]
      summary: List webhook endpoints
      description: Returns the team's webhook endpoints. Requires the `webhooks:read` scope.
      operationId: listWebhookEndpoints
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - $ref: "#/components/parameters/EndingBefore"
      responses:
        "200":
          description: A page of webhook endpoints.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, enum: [list] }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookEndpoint" }
                  has_more: { type: boolean }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [webhook_endpoints]
      summary: Create a webhook endpoint
      description: Registers a new webhook endpoint. Requires the `webhooks:write` scope.
      operationId: createWebhookEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEndpointCreate" }
      responses:
        "201":
          description: The created webhook endpoint (includes the signing secret once).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /webhook_endpoints/{id}:
    parameters:
      - $ref: "#/components/parameters/WebhookEndpointId"
    get:
      tags: [webhook_endpoints]
      summary: Retrieve a webhook endpoint
      description: Returns a single webhook endpoint. Requires the `webhooks:read` scope.
      operationId: getWebhookEndpoint
      responses:
        "200":
          description: The webhook endpoint.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [webhook_endpoints]
      summary: Update a webhook endpoint
      description: Updates the URL or subscribed events. Requires the `webhooks:write` scope.
      operationId: updateWebhookEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEndpointCreate" }
      responses:
        "200":
          description: The updated webhook endpoint.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [webhook_endpoints]
      summary: Delete a webhook endpoint
      description: Deletes a webhook endpoint. Requires the `webhooks:write` scope.
      operationId: deleteWebhookEndpoint
      responses:
        "204":
          description: Deleted. No content.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhook_endpoints/{id}/rotate:
    parameters:
      - $ref: "#/components/parameters/WebhookEndpointId"
    post:
      tags: [webhook_endpoints]
      summary: Rotate a webhook signing secret
      description: Generates a new signing secret for the endpoint and returns it once. Requires the `webhooks:write` scope.
      operationId: rotateWebhookEndpointSecret
      responses:
        "200":
          description: The endpoint with its new signing secret.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /runs:
    get:
      tags: [runs]
      summary: List measurement runs
      description: |
        Returns a paginated list of measurement runs for one tracked entity,
        newest first. The `tracked_entity_id` query parameter is required and
        must reference an entity the key can read. Requires the `entities:read`
        scope.
      operationId: listRuns
      parameters:
        - $ref: "#/components/parameters/TrackedEntityIdQuery"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - name: status
          in: query
          description: Optional filter on run status.
          schema: { type: string }
      responses:
        "200":
          description: A page of runs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, enum: [list] }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Run" }
                  has_more: { type: boolean }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /visibility:
    get:
      tags: [visibility]
      summary: List visibility snapshots
      description: |
        Returns a paginated list of visibility snapshots (the aggregated
        SeenRate trend record) for one tracked entity, newest first. The
        `tracked_entity_id` query parameter is required; optionally filter to a
        single run with `run_id`. Requires the `entities:read` scope.
      operationId: listVisibilitySnapshots
      parameters:
        - $ref: "#/components/parameters/TrackedEntityIdQuery"
        - name: run_id
          in: query
          description: Optional filter to the snapshot(s) for one run, prefixed with `run_`.
          schema: { type: string }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
      responses:
        "200":
          description: A page of visibility snapshots.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, enum: [list] }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/VisibilitySnapshot" }
                  has_more: { type: boolean }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /widget:
    get:
      tags: [visibility]
      summary: Read the plugin widget snapshot
      description: |
        Returns the latest headline metrics for the single tracked entity a
        **plugin key** (`pk_…`) is bound to: SeenRate, per-engine scores, share
        of voice, which engines ran, and the measurement time. It is a strict
        subset of `/visibility` (a short headline trend, no cursors or methodology internals), plus
        a confidence label and the top open fixes for the plugin's admin page.

        This endpoint is reachable **only** by a plugin-scoped read-only key,
        which carries the `widget:read` scope and no other. Unlike the rest of
        the API it is **not** gated on a Pro plan, so a connected WordPress
        plugin on a Starter-tier account can still populate its wp-admin widget.
        The entity is fixed at mint time (bound to the key): there is no
        entity query parameter, and no other key type can call this route.
      operationId: readPluginWidget
      responses:
        "200":
          description: The entity's latest headline widget metrics.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, enum: [widget] }
                  tracked_entity_id: { type: string }
                  name: { type: string }
                  seenrate: { type: number, nullable: true, description: "0–100 headline SeenRate." }
                  per_engine_scores: { type: object, additionalProperties: { type: number } }
                  share_of_voice: { type: number, nullable: true, description: "0–1." }
                  engines_run: { type: array, items: { type: string } }
                  measured_at: { type: string, nullable: true, format: date-time }
                  confidence: { type: string, nullable: true, enum: [confirmed, directional], description: "Directional when the latest run had too few usable answers to be confident." }
                  trend:
                    type: array
                    description: Up to 12 recent headline points, oldest first.
                    items:
                      type: object
                      properties:
                        seenrate: { type: number, nullable: true }
                        measured_at: { type: string, format: date-time }
                        method_changed: { type: boolean, description: "True where the measurement method changed since the previous point." }
                  open_fixes: { type: integer, description: "Open items in the entity's fix plan." }
                  top_fixes:
                    type: array
                    description: Up to 3 open fixes, most important first.
                    items:
                      type: object
                      properties:
                        title: { type: string }
                        type: { type: string, nullable: true }
                        impact: { type: string, nullable: true, enum: [high, med, low] }
                        page_url: { type: string, nullable: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

# ------------------------------------------------------------- components
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Send your API key as a Bearer token:

            Authorization: Bearer frontpagely_live_sk_4Xz9...

  parameters:
    Limit:
      name: limit
      in: query
      description: Page size. 1–100. Defaults to 25.
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    StartingAfter:
      name: starting_after
      in: query
      description: |
        Cursor for forward pagination. Pass the `id` of the last
        item from the previous page to fetch the next page.
      schema: { type: string }
    EndingBefore:
      name: ending_before
      in: query
      description: |
        Cursor for backward pagination. Pass the `id` of the first
        item from the current page to fetch the previous page.
      schema: { type: string }
    EntityId:
      name: id
      in: path
      required: true
      description: A tracked-entity id, prefixed with `ent_`.
      schema: { type: string, pattern: "^ent_[A-Za-z0-9_-]{8,}$" }
    WebhookEndpointId:
      name: id
      in: path
      required: true
      description: A webhook-endpoint id, prefixed with `whe_`.
      schema: { type: string, pattern: "^whe_[A-Za-z0-9_-]{8,}$" }
    TrackedEntityIdQuery:
      name: tracked_entity_id
      in: query
      required: true
      description: A tracked-entity id, prefixed with `ent_`. The entity must be readable by the key.
      schema: { type: string, pattern: "^ent_[A-Za-z0-9_-]{8,}$" }

  responses:
    BadRequest:
      description: A required field was missing or a field value was rejected.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: API key missing, malformed, or rejected.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: The key is valid but lacks the scope required for this route.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: No resource with that id is visible to this key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type: { type: string }
            message: { type: string }
            param: { type: string }
    TrackedEntity:
      type: object
      properties:
        object: { type: string, enum: [tracked_entity] }
        id: { type: string, pattern: "^ent_[A-Za-z0-9_-]{8,}$" }
        name: { type: string }
        domain: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    TrackedEntityCreate:
      type: object
      required: [name]
      properties:
        name: { type: string, description: Display name of the brand, firm, or site to track. }
        domain: { type: string, description: Primary domain for the entity, e.g. example.com. }
    WebhookEndpoint:
      type: object
      properties:
        object: { type: string, enum: [webhook_endpoint] }
        id: { type: string, pattern: "^whe_[A-Za-z0-9_-]{8,}$" }
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string }
        signing_secret:
          type: string
          description: Returned only on create and rotate.
        created_at: { type: string, format: date-time }
    WebhookEndpointCreate:
      type: object
      required: [url, events]
      properties:
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string }
          description: Subscribed event types (see the webhooks.js EVENT_TYPES taxonomy).
    Run:
      type: object
      properties:
        object: { type: string, enum: [run] }
        id: { type: string, pattern: "^run_[A-Za-z0-9_-]{8,}$" }
        tracked_entity_id: { type: string, pattern: "^ent_[A-Za-z0-9_-]{8,}$" }
        trigger: { type: string, nullable: true, description: What initiated the run (e.g. schedule, manual). }
        status: { type: string, nullable: true, description: Lifecycle status of the run. }
        engines_requested:
          type: array
          nullable: true
          items: { type: string }
          description: The engines this run was planned to query.
        samples_per_prompt: { type: integer, nullable: true }
        started_at: { type: string, format: date-time, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
    VisibilitySnapshot:
      type: object
      description: The C1 aggregated SeenRate trend record (methodology §1.4 / §4.5). Every row is stamped with score_methodology_version (§6.1 / C6).
      properties:
        object: { type: string, enum: [visibility_snapshot] }
        id: { type: string, pattern: "^snap_[A-Za-z0-9_-]{8,}$" }
        tracked_entity_id: { type: string, pattern: "^ent_[A-Za-z0-9_-]{8,}$" }
        run_id: { type: string, pattern: "^run_[A-Za-z0-9_-]{8,}$" }
        seenrate: { type: number, description: 0–100 headline SeenRate. }
        per_engine_scores:
          type: object
          additionalProperties: { type: number }
          description: Per-engine SeenRate, e.g. { chatgpt 62, perplexity 56, claude 38 }.
        components:
          type: object
          additionalProperties: { type: number }
          description: The M/P/SV/C/O component scores (0–1).
        share_of_voice: { type: number, nullable: true, description: 0–1 share of voice. }
        n_effective: { type: number }
        engines_run:
          type: array
          items: { type: string }
          description: The engines that actually ran (drives §4.3 comparability).
        trust:
          type: object
          nullable: true
          description: Per-engine parser trust, or null.
        directional:
          type: object
          nullable: true
          description: Directional flags (e.g. engines, location_directional).
        score_methodology_version: { type: integer, description: FK to score_methodology_versions, stamped on every row. }
        created_at: { type: string, format: date-time }
