openapi: 3.1.0
info:
  title: XenonFlare Developer API
  version: "1.0.0"
  description: |
    Organization-scoped REST API for automating SEO workflows.
    Authenticate with `Authorization: Bearer xf_live_...` API keys created in the web app.

servers:
  - url: https://api.xenonflare.com/api/v1
    description: Production
  - url: http://localhost:4000/api/v1
    description: Local development

tags:
  - name: System
  - name: Organization
  - name: Properties
  - name: Jobs
  - name: Reports
  - name: Usage
  - name: Search Console
  - name: Webhooks

paths:
  /health:
    get:
      tags: [System]
      summary: Health check
      security: []
      responses:
        "200":
          description: API is reachable
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/HealthData"
              example:
                data:
                  status: ok
                  version: v1
                  apiVersion: "1.0.0"
                  timestamp: "2026-06-07T12:00:00.000Z"
                  statusPage: https://status.xenonflare.com
                meta:
                  requestId: req_health_001

  /organization:
    get:
      tags: [Organization]
      summary: Organization overview
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Organization summary and usage totals
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/OrganizationData"
              example:
                data:
                  organization:
                    id: 507f1f77bcf86cd799439011
                    name: Acme Marketing
                    plan: starter
                  usage:
                    plan: starter
                    periodLabel: Jun 2026
                  totals:
                    properties: 3
                    activeJobs: 1
                meta:
                  requestId: req_org_001
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /organization/gsc:
    get:
      tags: [Search Console]
      summary: Organization GSC portfolio summary
      description: |
        Cached Search Console totals across linked properties. Connect GSC in the web app first —
        this endpoint reads stored snapshots only (no live Google API calls).
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Portfolio-level GSC summary
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/OrganizationGscData"
              example:
                data:
                  configured: true
                  orgConnected: true
                  linkedProperties: 3
                  totalProperties: 5
                  totalClicks28d: 1240
                  totalImpressions28d: 48200
                  staleSyncCount: 1
                  tokenRevoked: false
                meta:
                  requestId: req_org_gsc_001

  /properties:
    get:
      tags: [Properties]
      summary: List properties
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: include
          in: query
          schema:
            type: string
          description: Comma-separated enrichments — `gsc`, `lastScan`
      responses:
        "200":
          description: Paginated property list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/PropertyListData"
              example:
                data:
                  properties:
                    - id: 507f1f77bcf86cd799439012
                      name: example.com
                      url: https://example.com
                      verified: true
                      createdAt: "2026-06-01T10:00:00.000Z"
                meta:
                  requestId: req_props_001
                  hasMore: false
                  nextCursor: null
    post:
      tags: [Properties]
      summary: Create property
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  example: https://example.com
                name:
                  type: string
                  example: Example marketing site
      responses:
        "201":
          description: Property created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/PropertyCreateData"
              example:
                data:
                  property:
                    id: 507f1f77bcf86cd799439015
                    name: Example marketing site
                    url: https://example.com
                    verified: false
                    createdAt: "2026-06-07T14:00:00.000Z"
                meta:
                  requestId: req_prop_create_001
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /properties/{propertyId}:
    get:
      tags: [Properties]
      summary: Get property
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/PropertyId"
        - name: include
          in: query
          schema:
            type: string
          description: Comma-separated enrichments — `gsc`, `lastScan`
      responses:
        "200":
          description: Property detail
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/PropertyDetailData"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /properties/{propertyId}/gsc:
    get:
      tags: [Search Console]
      summary: Property GSC snapshot
      description: |
        Returns cached 28-day Search Console metrics (queries, pages, daily series, crawl correlation)
        for a linked property. Link GSC in the web app — API keys cannot run OAuth connect.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/PropertyId"
      responses:
        "200":
          description: GSC status and cached snapshot (snapshot null when not linked)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/DeveloperPropertyGscResponse"
        "404":
          $ref: "#/components/responses/NotFound"

  /properties/{propertyId}/issues:
    get:
      tags: [Properties]
      summary: Property SEO issues
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/PropertyId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - name: scanKind
          in: query
          schema:
            type: string
            enum: [site, page]
        - name: status
          in: query
          schema:
            type: string
            enum: [open, resolved, fixed]
      responses:
        "200":
          description: Issue lifecycle summary and paginated issues
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/PropertyIssuesData"

  /properties/{propertyId}/gsc/overview:
    get:
      tags: [Search Console]
      summary: GSC overview totals
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/PropertyId"
      responses:
        "200":
          description: Connection status and overview totals
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/GscOverviewData"

  /properties/{propertyId}/gsc/queries:
    get:
      tags: [Search Console]
      summary: GSC top queries (paginated)
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/PropertyId"
        - $ref: "#/components/parameters/Limit"
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated query rows from cached snapshot
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/GscQueriesData"

  /properties/{propertyId}/gsc/pages:
    get:
      tags: [Search Console]
      summary: GSC top pages (paginated)
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/PropertyId"
        - $ref: "#/components/parameters/Limit"
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        "200":
          description: Paginated page rows from cached snapshot
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/GscPagesData"

  /properties/{propertyId}/gsc/sync:
    post:
      tags: [Search Console]
      summary: Refresh GSC snapshot
      description: Triggers a live Google Search Console fetch using the org OAuth token linked in the web app.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/PropertyId"
      responses:
        "202":
          description: Sync completed
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/GscSyncData"
              example:
                data:
                  synced: true
                  syncedAt: "2026-06-07T15:30:00.000Z"
                meta:
                  requestId: req_gsc_sync_001
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /properties/{propertyId}/scans:
    post:
      tags: [Jobs]
      summary: Queue site SEO scan
      description: Queues a multi-page `seo-scan` job for the property.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/PropertyId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                maxPages:
                  type: integer
                  minimum: 1
                maxDepth:
                  type: integer
                  minimum: 1
      responses:
        "202":
          description: Job queued
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/ScanQueuedData"
              example:
                data:
                  job:
                    id: 507f1f77bcf86cd799439014
                    status: pending
                    type: seo-scan
                    propertyId: 507f1f77bcf86cd799439012
                    createdAt: "2026-06-07T12:00:00.000Z"
                  crawlTier: verified
                  effectiveLimits:
                    maxPages: 500
                    maxDepth: 8
                  message: Website SEO scan queued.
                meta:
                  requestId: req_scan_001
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /reports:
    get:
      tags: [Reports]
      summary: List shared audit reports
      description: Returns non-expired audit share links created in the web app.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Paginated report list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/ReportListData"
              example:
                data:
                  reports:
                    - token: abc123sharetoken
                      propertyId: 507f1f77bcf86cd799439012
                      propertyName: example.com
                      propertyUrl: https://example.com
                      scanId: 507f1f77bcf86cd799439013
                      expiresAt: "2026-07-07T12:00:00.000Z"
                      viewCount: 4
                      createdAt: "2026-06-07T12:00:00.000Z"
                      publicUrl: https://app.xenonflare.com/reports/abc123sharetoken
                meta:
                  requestId: req_reports_001
                  hasMore: false
                  nextCursor: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /reports/{token}:
    get:
      tags: [Reports]
      summary: Get shared report metadata
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: token
          in: path
          required: true
          schema:
            type: string
          example: abc123sharetoken
      responses:
        "200":
          description: Report metadata and public URL
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/ReportDetailData"
              example:
                data:
                  report:
                    token: abc123sharetoken
                    propertyId: 507f1f77bcf86cd799439012
                    propertyName: example.com
                    propertyUrl: https://example.com
                    scanId: 507f1f77bcf86cd799439013
                    score: 82
                    pagesScanned: 124
                    scannedAt: "2026-06-07T11:55:00.000Z"
                    expiresAt: "2026-07-07T12:00:00.000Z"
                    expired: false
                    viewCount: 4
                    createdAt: "2026-06-07T12:00:00.000Z"
                    publicUrl: https://app.xenonflare.com/reports/abc123sharetoken
                meta:
                  requestId: req_report_001
        "404":
          $ref: "#/components/responses/NotFound"

  /properties/{propertyId}/crawls:
    post:
      tags: [Jobs]
      summary: Queue site crawl
      description: Alias of `/scans` — website crawls run as `seo-scan` jobs.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/PropertyId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                maxPages:
                  type: integer
                maxDepth:
                  type: integer
      responses:
        "202":
          description: Job queued
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/ScanQueuedData"
              example:
                data:
                  job:
                    id: 507f1f77bcf86cd799439014
                    status: pending
                    type: seo-scan
                    propertyId: 507f1f77bcf86cd799439012
                    progress: 0
                    createdAt: "2026-06-07T12:00:00.000Z"
                  crawlTier: verified
                  effectiveLimits:
                    maxPages: 500
                    maxDepth: 8
                  message: Website crawl queued.
                meta:
                  requestId: req_crawl_001
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /jobs:
    get:
      tags: [Jobs]
      summary: List jobs
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/JobStatus"
        - $ref: "#/components/parameters/JobPropertyId"
        - $ref: "#/components/parameters/JobType"
      responses:
        "200":
          description: Paginated jobs
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/JobListData"
              example:
                data:
                  jobs:
                    - id: 507f1f77bcf86cd799439014
                      type: seo-scan
                      status: completed
                      progress: 100
                      propertyId: 507f1f77bcf86cd799439012
                      propertyName: example.com
                      tenantType: website
                      tenantLabel: example.com
                      shopDomain: null
                      createdAt: "2026-06-07T12:00:00.000Z"
                      startedAt: "2026-06-07T12:00:05.000Z"
                      completedAt: "2026-06-07T12:05:12.000Z"
                      error: null
                meta:
                  requestId: req_jobs_001
                  hasMore: false
                  nextCursor: null

  /jobs/{jobId}:
    get:
      tags: [Jobs]
      summary: Get job
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: jobId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Job detail
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/JobDetailData"
              example:
                data:
                  job:
                    id: 507f1f77bcf86cd799439014
                    type: seo-scan
                    status: completed
                    propertyId: 507f1f77bcf86cd799439012
                    result:
                      score: 82
                      pagesScanned: 124
                      issuesCount: 17
                    createdAt: "2026-06-07T12:00:00.000Z"
                    completedAt: "2026-06-07T12:05:12.000Z"
                meta:
                  requestId: req_job_detail_001
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /jobs/{jobId}/cancel:
    post:
      tags: [Jobs]
      summary: Cancel a pending or processing job
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: jobId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Job cancelled
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/JobCancelData"
              example:
                data:
                  job:
                    id: 507f1f77bcf86cd799439014
                    type: seo-scan
                    status: cancelled
                    progress: 42
                    propertyId: 507f1f77bcf86cd799439012
                    createdAt: "2026-06-07T12:00:00.000Z"
                    completedAt: "2026-06-07T12:02:00.000Z"
                  message: Job cancelled.
                meta:
                  requestId: req_job_cancel_001
        "400":
          $ref: "#/components/responses/ValidationError"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /scans/batch:
    post:
      tags: [Jobs]
      summary: Queue multiple scans in one request
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [scans]
              properties:
                scans:
                  type: array
                  maxItems: 25
                  items:
                    type: object
                    required: [propertyId]
                    properties:
                      propertyId:
                        type: string
                      maxPages:
                        type: integer
                      maxDepth:
                        type: integer
      responses:
        "202":
          description: Batch accepted — per-item success or error in results
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/BatchScanData"
              example:
                data:
                  results:
                    - propertyId: 507f1f77bcf86cd799439012
                      job:
                        id: 507f1f77bcf86cd799439014
                        type: seo-scan
                        status: pending
                        progress: 0
                        propertyId: 507f1f77bcf86cd799439012
                        createdAt: "2026-06-07T12:00:00.000Z"
                      error: null
                meta:
                  requestId: req_batch_001
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /usage:
    get:
      tags: [Usage]
      summary: Usage summary
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Current period usage
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/UsageData"
              example:
                data:
                  usage:
                    period: "2026-06"
                    plan: starter
                    counters:
                      scans: 12
                      aiInputTokens: 0
                      aiOutputTokens: 0
                      redirectsCreated: 0
                      imagesOptimized: 0
                    limits:
                      scansPerMonth: 50
                      aiCreditsPerMonth: 100
                  activity:
                    period: "2026-06"
                    totals:
                      jobs: 12
                      completed: 11
                      failed: 1
                    byType:
                      seo-scan:
                        total: 12
                        completed: 11
                        failed: 1
                  crawlLimits:
                    maxPages: 500
                    maxDepth: 8
                meta:
                  requestId: req_usage_001

  /limits:
    get:
      tags: [Usage]
      summary: Plan limits
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Limits and meters for the workspace plan
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/LimitsData"
              example:
                data:
                  plan: starter
                  period:
                    start: "2026-06-01"
                    end: "2026-06-30"
                  developerApi:
                    readPerHour: 1000
                    writePerHour: 60
                    orgWriteDaily: 500
                    maxActiveKeys: 5
                    rateLimitHeaders:
                      read: X-Developer-Api-RateLimit-read-Remaining
                      write: X-Developer-Api-RateLimit-write-Remaining
                meta:
                  requestId: req_limits_001

  /rate-limits:
    get:
      tags: [Usage]
      summary: Live API rate limit counters
      description: Returns remaining read/write and org-wide daily write quota for the current API key.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Current rate limit state
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/RateLimitsData"

  /webhooks:
    get:
      tags: [Webhooks]
      summary: List webhook endpoints
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Webhook list
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/WebhookListData"
              example:
                data:
                  webhooks:
                    - id: 507f1f77bcf86cd799439015
                      name: Production job callbacks
                      url: https://example.com/webhooks/xenonflare
                      secretPrefix: whsec_
                      events: [job.completed, job.failed]
                      enabled: true
                      lastDeliveryAt: "2026-06-07T11:00:00.000Z"
                      lastDeliveryStatus: 200
                      createdAt: "2026-06-01T10:00:00.000Z"
                      updatedAt: "2026-06-07T11:00:00.000Z"
                meta:
                  requestId: req_webhooks_list_001
    post:
      tags: [Webhooks]
      summary: Create webhook endpoint
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url]
              properties:
                name:
                  type: string
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    enum: [job.completed, job.failed, job.cancelled]
                enabled:
                  type: boolean
      responses:
        "201":
          description: Webhook created — signingSecret shown once
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/DeveloperWebhookCreateResponse"
              example:
                data:
                  webhook:
                    id: 507f1f77bcf86cd799439015
                    name: Production job callbacks
                    url: https://example.com/webhooks/xenonflare
                    secretPrefix: whsec_
                    events: [job.completed, job.failed]
                    enabled: true
                    lastDeliveryAt: null
                    lastDeliveryStatus: null
                  secret: whsec_live_secret_shown_once
                meta:
                  requestId: req_webhook_create_001
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /webhooks/{webhookId}:
    patch:
      tags: [Webhooks]
      summary: Update webhook endpoint
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    enum: [job.completed, job.failed, job.cancelled]
                enabled:
                  type: boolean
      responses:
        "200":
          description: Updated webhook
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/WebhookUpdateData"
              example:
                data:
                  webhook:
                    id: 507f1f77bcf86cd799439015
                    name: Production job callbacks
                    url: https://example.com/webhooks/xenonflare
                    secretPrefix: whsec_
                    events: [job.completed, job.failed, job.cancelled]
                    enabled: false
                    lastDeliveryAt: "2026-06-07T11:00:00.000Z"
                    lastDeliveryStatus: 200
                meta:
                  requestId: req_webhook_patch_001
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    delete:
      tags: [Webhooks]
      summary: Delete webhook endpoint
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Webhook deleted
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/DeletedData"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /webhooks/{webhookId}/test:
    post:
      tags: [Webhooks]
      summary: Send test delivery
      description: |
        Posts a signed sample payload to the webhook URL. The body includes `test: true` and a synthetic job.
        Does not consume job quota. Logged in delivery history.
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  enum: [job.completed, job.failed, job.cancelled]
                  default: job.completed
      responses:
        "200":
          description: Test delivery attempted
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/WebhookTestDeliveryData"
              example:
                data:
                  delivery:
                    id: 507f1f77bcf86cd799439016
                    webhookId: 507f1f77bcf86cd799439015
                    event: job.completed
                    statusCode: 200
                    success: true
                    durationMs: 128
                    createdAt: "2026-06-07T12:00:00.000Z"
                meta:
                  requestId: req_webhook_test_001
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /webhooks/{webhookId}/rotate-secret:
    post:
      tags: [Webhooks]
      summary: Rotate webhook signing secret
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: New signing secret — shown once
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/DeveloperWebhookCreateResponse"
              example:
                data:
                  webhook:
                    id: 507f1f77bcf86cd799439015
                    name: Production job callbacks
                    url: https://example.com/webhooks/xenonflare
                    secretPrefix: whsec_
                    events: [job.completed, job.failed]
                    enabled: true
                    lastDeliveryAt: "2026-06-07T11:00:00.000Z"
                    lastDeliveryStatus: 200
                  secret: whsec_rotated_secret_shown_once
                meta:
                  requestId: req_webhook_rotate_001
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /webhooks/{webhookId}/deliveries:
    get:
      tags: [Webhooks]
      summary: Recent webhook delivery attempts
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/RequestId"
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
      responses:
        "200":
          description: Delivery log (retained 30 days)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/DeveloperWebhookDeliveriesData"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Live API key (`xf_live_...`) or sandbox test key (`xf_test_...`).
        Test keys authenticate normally; write endpoints return deterministic mock responses with no side effects.

  parameters:
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      schema:
        type: string
      description: Optional client correlation ID echoed in `meta.requestId` on all responses.
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 128
      description: Optional key for safe retries on write endpoints (24h TTL).
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 40
    Cursor:
      name: cursor
      in: query
      schema:
        type: string
    PropertyId:
      name: propertyId
      in: path
      required: true
      schema:
        type: string
    JobStatus:
      name: status
      in: query
      schema:
        type: string
        enum: [pending, processing, completed, failed, cancelled]
    JobPropertyId:
      name: propertyId
      in: query
      schema:
        type: string
      description: Filter jobs to a website property ID in your workspace.
    JobType:
      name: type
      in: query
      schema:
        type: string
        example: seo-scan

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          example:
            error:
              code: api_key_invalid
              message: Invalid or revoked API key.
            meta:
              requestId: req_auth_001
    Forbidden:
      description: Insufficient scope or plan
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          example:
            error:
              code: plan_required
              message: Developer API requires a Starter or Growth plan.
            meta:
              requestId: req_forbidden_001
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          example:
            error:
              code: not_found
              message: Property not found
            meta:
              requestId: req_notfound_001
    ValidationError:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          example:
            error:
              code: validation_error
              message: url is required
            meta:
              requestId: req_validation_001
    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds until the current rate-limit window resets
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
          example:
            error:
              code: rate_limit
              message: Rate limit exceeded — try again later
            meta:
              requestId: req_ratelimit_001

  schemas:
    SuccessEnvelope:
      type: object
      required: [data]
      properties:
        data:
          type: object
        meta:
          $ref: "#/components/schemas/ApiMeta"

    ApiMeta:
      type: object
      properties:
        requestId:
          type: string
        hasMore:
          type: boolean
        nextCursor:
          type: string
          nullable: true

    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - unauthorized
                - forbidden
                - insufficient_scope
                - plan_required
                - rate_limit
                - not_found
                - validation_error
                - internal_error
                - api_key_invalid
            message:
              type: string
        meta:
          type: object
          properties:
            requestId:
              type: string

    DeveloperApiJobResult:
      type: object
      properties:
        scanId:
          type: string
          nullable: true
        score:
          type: number
          nullable: true
        issuesCount:
          type: integer
          nullable: true
        pagesScanned:
          type: integer
          nullable: true
        pagesFailed:
          type: integer
          nullable: true
        pagesSkipped:
          type: integer
          nullable: true
        stopReason:
          type: string
          nullable: true
        maxPages:
          type: integer
          nullable: true
        urlsAttempted:
          type: integer
          nullable: true
        categoryScores:
          type: object
          additionalProperties:
            type: number
          nullable: true
        shareUrl:
          type: string
          nullable: true

    DeveloperJobDetail:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
        status:
          type: string
          enum: [pending, processing, completed, failed, cancelled]
        progress:
          type: number
        propertyId:
          type: string
          nullable: true
        result:
          $ref: "#/components/schemas/DeveloperApiJobResult"
        error:
          type: string
          nullable: true

    RateLimitBucket:
      type: object
      properties:
        limit:
          type: integer
        remaining:
          type: integer
        used:
          type: integer
        resetAt:
          type: string
          format: date-time
          nullable: true
        windowSeconds:
          type: integer

    RateLimitsData:
      type: object
      properties:
        rateLimits:
          type: object
          properties:
            read:
              $ref: "#/components/schemas/RateLimitBucket"
            write:
              $ref: "#/components/schemas/RateLimitBucket"
            orgWriteDaily:
              $ref: "#/components/schemas/RateLimitBucket"

    DeveloperWebhook:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        url:
          type: string
          format: uri
        secretPrefix:
          type: string
        events:
          type: array
          items:
            type: string
            enum: [job.completed, job.failed, job.cancelled]
        enabled:
          type: boolean
        lastDeliveryAt:
          type: string
          format: date-time
          nullable: true
        lastDeliveryStatus:
          type: integer
          nullable: true

    TenantSeoIssue:
      type: object
      properties:
        id:
          type: string
        issueKey:
          type: string
        severity:
          type: string
          enum: [critical, warning, info]
        title:
          type: string
        description:
          type: string
        pageUrl:
          type: string
          nullable: true
        status:
          type: string
        workflow:
          type: string

    PropertyIssuesData:
      type: object
      properties:
        summary:
          type: object
        issues:
          type: array
          items:
            $ref: "#/components/schemas/TenantSeoIssue"
        meta:
          $ref: "#/components/schemas/ApiMeta"

    GscTotals:
      type: object
      properties:
        clicks:
          type: number
        impressions:
          type: number
        ctr:
          type: number
        position:
          type: number

    GscOverviewData:
      type: object
      properties:
        configured:
          type: boolean
        status:
          type: object
        overview:
          type: object
          nullable: true
          properties:
            totals:
              $ref: "#/components/schemas/GscTotals"
            rangeStart:
              type: string
            rangeEnd:
              type: string
            syncedAt:
              type: string
              format: date-time

    HealthData:
      type: object
      properties:
        status:
          type: string
          enum: [ok]
        version:
          type: string
        apiVersion:
          type: string
        timestamp:
          type: string
          format: date-time
        statusPage:
          type: string
          format: uri

    OrganizationData:
      type: object
      properties:
        organization:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            plan:
              type: string
        usage:
          type: object
        totals:
          type: object

    OrganizationGscData:
      type: object
      description: Portfolio-level GSC summary across linked properties
      required:
        - configured
        - orgConnected
        - linkedProperties
        - totalProperties
        - totalClicks28d
        - totalImpressions28d
        - staleSyncCount
        - tokenRevoked
      properties:
        configured:
          type: boolean
        orgConnected:
          type: boolean
        linkedProperties:
          type: integer
        totalProperties:
          type: integer
        totalClicks28d:
          type: number
        totalImpressions28d:
          type: number
        staleSyncCount:
          type: integer
        tokenRevoked:
          type: boolean

    DeveloperPropertyGscStatus:
      type: object
      properties:
        orgConnected:
          type: boolean
        propertyLinked:
          type: boolean
        gscSiteUrl:
          type: string
          nullable: true
        linkedAt:
          type: string
          format: date-time
          nullable: true
        lastSyncAt:
          type: string
          format: date-time
          nullable: true
        tokenStatus:
          type: string
          enum: [ok, revoked, missing]
        lastSyncError:
          type: string
          nullable: true

    DeveloperPropertyGscResponse:
      type: object
      properties:
        configured:
          type: boolean
        status:
          $ref: "#/components/schemas/DeveloperPropertyGscStatus"
        snapshot:
          oneOf:
            - $ref: "#/components/schemas/PropertyGscSnapshotData"
            - type: "null"
        history:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
              clicks:
                type: number
              impressions:
                type: number

    DeveloperProperty:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        url:
          type: string
        verified:
          type: boolean
        createdAt:
          type: string
          format: date-time
        lastScanAt:
          type: string
          format: date-time
          nullable: true
        lastScore:
          type: number
          nullable: true

    PropertyListData:
      type: object
      properties:
        properties:
          type: array
          items:
            $ref: "#/components/schemas/DeveloperProperty"

    PropertyCreateData:
      type: object
      properties:
        property:
          $ref: "#/components/schemas/DeveloperProperty"

    PropertyDetailData:
      type: object
      properties:
        property:
          type: object
          additionalProperties: true

    PropertyGscSnapshotData:
      type: object
      description: Cached GSC snapshot for a property
      properties:
        gscSiteUrl:
          type: string
        syncedAt:
          type: string
          format: date-time
        rangeStart:
          type: string
        rangeEnd:
          type: string
        totals:
          $ref: "#/components/schemas/GscTotals"
        daily:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
              clicks:
                type: number
              impressions:
                type: number
              ctr:
                type: number
              position:
                type: number
        topQueries:
          type: array
          items:
            type: object
            properties:
              query:
                type: string
              clicks:
                type: number
              impressions:
                type: number
              ctr:
                type: number
              position:
                type: number
        topPages:
          type: array
          items:
            type: object
            properties:
              page:
                type: string
              clicks:
                type: number
              impressions:
                type: number
              ctr:
                type: number
              position:
                type: number
        indexSummary:
          type: object
          properties:
            sitemapCount:
              type: integer
            sitemapErrors:
              type: integer
            sitemapWarnings:
              type: integer

    GscSyncData:
      type: object
      properties:
        synced:
          type: boolean
        syncedAt:
          type: string
          format: date-time
          nullable: true

    ScanQueuedData:
      type: object
      properties:
        job:
          $ref: "#/components/schemas/DeveloperQueuedJob"
        crawlTier:
          type: string
        effectiveLimits:
          type: object
          properties:
            maxPages:
              type: integer
            maxDepth:
              type: integer
        message:
          type: string

    DeveloperQueuedJob:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
        status:
          type: string
        progress:
          type: number
        propertyId:
          type: string
        createdAt:
          type: string
          format: date-time

    JobSummary:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
        status:
          type: string
        progress:
          type: number
        propertyId:
          type: string
          nullable: true
        propertyName:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time

    JobListData:
      type: object
      properties:
        jobs:
          type: array
          items:
            $ref: "#/components/schemas/JobSummary"

    JobDetailData:
      type: object
      properties:
        job:
          $ref: "#/components/schemas/DeveloperJobDetail"

    JobCancelData:
      type: object
      properties:
        job:
          $ref: "#/components/schemas/DeveloperJobDetail"
        message:
          type: string

    WebhookListData:
      type: object
      properties:
        webhooks:
          type: array
          items:
            $ref: "#/components/schemas/DeveloperWebhook"

    ReportSummary:
      type: object
      properties:
        token:
          type: string
        propertyId:
          type: string
        propertyName:
          type: string
        propertyUrl:
          type: string
        scanId:
          type: string
        expiresAt:
          type: string
          format: date-time
        viewCount:
          type: integer
        createdAt:
          type: string
          format: date-time
        publicUrl:
          type: string

    ReportListData:
      type: object
      properties:
        reports:
          type: array
          items:
            $ref: "#/components/schemas/ReportSummary"

    ReportDetailData:
      type: object
      properties:
        report:
          type: object
          additionalProperties: true

    UsageData:
      type: object
      properties:
        usage:
          $ref: "#/components/schemas/UsageSummary"
        activity:
          $ref: "#/components/schemas/JobActivitySummary"
        usageDetail:
          $ref: "#/components/schemas/OrgUsageDetail"
        crawlLimits:
          type: object
          properties:
            maxPages:
              type: integer
            maxDepth:
              type: integer

    UsageSummary:
      type: object
      properties:
        period:
          type: string
        plan:
          type: string
        counters:
          type: object
          properties:
            scans:
              type: integer
            aiInputTokens:
              type: integer
            aiOutputTokens:
              type: integer
            redirectsCreated:
              type: integer
            imagesOptimized:
              type: integer
        limits:
          type: object
          properties:
            scansPerMonth:
              type: integer
              nullable: true
            aiCreditsPerMonth:
              type: integer
              nullable: true

    JobActivitySummary:
      type: object
      properties:
        period:
          type: string
        totals:
          type: object
          properties:
            jobs:
              type: integer
            completed:
              type: integer
            failed:
              type: integer
        byType:
          type: object
          additionalProperties:
            type: object
            properties:
              total:
                type: integer
              completed:
                type: integer
              failed:
                type: integer

    OrgUsageDetail:
      type: object
      properties:
        period:
          type: string
        web:
          type: object
          properties:
            plan:
              type: string
            counters:
              type: object
            limits:
              type: object
            crawlLimits:
              type: object
              properties:
                maxPages:
                  type: integer
                maxDepth:
                  type: integer
        shopify:
          type: array
          items:
            type: object

    LimitsData:
      type: object
      properties:
        plan:
          type: string
        period:
          type: object
          properties:
            start:
              type: string
            end:
              type: string
        usageDetail:
          $ref: "#/components/schemas/OrgUsageDetail"
        breakdown:
          $ref: "#/components/schemas/PlanLimitsBreakdown"
        developerApi:
          $ref: "#/components/schemas/DeveloperApiLimitsMeta"

    PlanLimitsBreakdown:
      type: object
      properties:
        plan:
          type: string
        meters:
          type: array
          items:
            type: object
        account:
          type: array
          items:
            type: object
        crawl:
          type: array
          items:
            type: object
        features:
          type: array
          items:
            type: object

    DeveloperApiLimitsMeta:
      type: object
      properties:
        readPerHour:
          type: integer
        writePerHour:
          type: integer
        orgWriteDaily:
          type: integer
        maxActiveKeys:
          type: integer
        rateLimitHeaders:
          type: object
          properties:
            read:
              type: string
            write:
              type: string

    WebhookUpdateData:
      type: object
      properties:
        webhook:
          $ref: "#/components/schemas/DeveloperWebhook"

    BatchScanData:
      type: object
      properties:
        results:
          type: array
          items:
            type: object
            properties:
              propertyId:
                type: string
              job:
                type: object
                nullable: true
              error:
                type: object
                nullable: true
                properties:
                  code:
                    type: string
                  message:
                    type: string

    GscQueriesData:
      type: object
      properties:
        queries:
          type: array
          items:
            type: object
            properties:
              query:
                type: string
              clicks:
                type: number
              impressions:
                type: number
              ctr:
                type: number
              position:
                type: number
        meta:
          $ref: "#/components/schemas/ApiMeta"

    GscPagesData:
      type: object
      properties:
        pages:
          type: array
          items:
            type: object
            properties:
              page:
                type: string
              clicks:
                type: number
              impressions:
                type: number
              ctr:
                type: number
              position:
                type: number
        meta:
          $ref: "#/components/schemas/ApiMeta"

    DeveloperWebhookCreateResponse:
      type: object
      properties:
        webhook:
          $ref: "#/components/schemas/DeveloperWebhook"
        signingSecret:
          type: string
          description: Shown once at creation or secret rotation

    DeletedData:
      type: object
      required: [deleted]
      properties:
        deleted:
          type: boolean
          example: true
        id:
          type: string
          description: ID of the deleted webhook endpoint

    WebhookTestDeliveryData:
      type: object
      required: [delivery]
      properties:
        delivery:
          type: object
          properties:
            event:
              type: string
              enum: [job.completed, job.failed, job.cancelled]
            statusCode:
              type: integer
            success:
              type: boolean
            durationMs:
              type: integer
            errorMessage:
              type: string
              nullable: true

    DeveloperWebhookDelivery:
      type: object
      properties:
        id:
          type: string
        event:
          type: string
        jobId:
          type: string
          nullable: true
        attempt:
          type: integer
        statusCode:
          type: integer
        success:
          type: boolean
        durationMs:
          type: integer
        errorMessage:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time

    DeveloperWebhookDeliveriesData:
      type: object
      properties:
        deliveries:
          type: array
          items:
            $ref: "#/components/schemas/DeveloperWebhookDelivery"

security:
  - bearerAuth: []
