openapi: 3.1.0
info:
  title: CacheBoost API
  version: "1"
  description: |
    The CacheBoost API lets you manage sites, sitemaps, boosts, and runs
    programmatically. All endpoints are under `/v1`.

servers:
  - url: https://api.cache-boost.com/v1
    description: Production

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API key in the format `cb_live_<32hex>`"

  schemas:
    Site:
      type: object
      properties:
        id:          { type: integer, example: 42 }
        domain:      { type: string,  example: "www.example.com" }
        validated:   { type: boolean, example: true }
        created_at:  { type: string,  format: date-time }

    SiteDetail:
      allOf:
        - $ref: "#/components/schemas/Site"
        - type: object
          properties:
            validation_token: { type: string, example: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" }

    SiteCreated:
      allOf:
        - $ref: "#/components/schemas/SiteDetail"
        - type: object
          properties:
            validated: { type: boolean, example: false }

    Sitemap:
      type: object
      properties:
        id:                { type: integer, example: 7 }
        url:               { type: string,  example: "https://www.example.com/sitemap.xml" }
        status:
          type: string
          enum: [waiting, downloading, parsing, ready, error]
          example: ready
        status_message:    { type: string,  nullable: true }
        last_processed_at: { type: string,  format: date-time, nullable: true }
        created_at:        { type: string,  format: date-time }

    Boost:
      type: object
      properties:
        id:                   { type: integer, example: 15 }
        site_id:              { type: integer, example: 42 }
        name:                 { type: string,  example: "Daily warm" }
        status:
          type: string
          enum: [pending, running, paused, done, failed]
          example: pending
        source_type:
          type: string
          enum: [sitemap, csv]
          example: sitemap
        region:
          type: array
          items: { type: string }
          example: ["fr"]
        url_root:             { type: string,  example: "/" }
        exclude_ext:          { type: array,   items: { type: string }, nullable: true }
        variations:           { type: object,  nullable: true }
        sitemap_ids:          { type: array,   items: { type: integer }, nullable: true }
        content_types:        { type: array,   items: { type: string },  nullable: true }
        ignore_errors:        { type: boolean, example: false }
        report_email_enabled: { type: boolean, example: true }
        schedule:             { type: string,  example: "0 1 * * *", nullable: true }
        next_run_at:          { type: string,  format: date-time, nullable: true }
        call_limit:           { type: integer, nullable: true }
        created_at:           { type: string,  format: date-time }

    RunSummary:
      type: object
      nullable: true
      properties:
        total_calls:              { type: integer, example: 1240 }
        successful_calls:         { type: integer, example: 1238 }
        failed_calls:             { type: integer, example: 2 }
        cache_hits:               { type: integer, example: 1165 }
        average_response_time_ms: { type: number,  example: 112.5 }
        pops:
          type: object
          additionalProperties: { type: integer }
          example: { "CDG": 450, "IAD": 390 }

    Run:
      type: object
      properties:
        id:             { type: integer, example: 88 }
        boost_id:       { type: integer, example: 15 }
        status:
          type: string
          enum: [pending, running, done, failed, paused]
          example: done
        status_message: { type: string,  nullable: true }
        started_at:     { type: string,  format: date-time, nullable: true }
        finished_at:    { type: string,  format: date-time, nullable: true }
        summary:
          $ref: "#/components/schemas/RunSummary"
        created_at:     { type: string,  format: date-time }

    RunDetail:
      allOf:
        - $ref: "#/components/schemas/Run"
        - type: object
          properties:
            boost_name: { type: string,  example: "Daily warm" }
            site_id:    { type: integer, example: 42 }

    Error:
      type: object
      properties:
        error: { type: string, example: "Description of the error." }

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Valid key but missing required scope
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found or not accessible with this key
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    UnprocessableEntity:
      description: Validation error
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

paths:

  # ── Sites ──────────────────────────────────────────────────────────────────

  /sites:
    get:
      operationId: listSites
      summary: List sites
      description: Returns all sites belonging to the authenticated user. If the API key is restricted to specific sites, only those sites are returned.
      tags: [Sites]
      security: [{ bearerAuth: [sites:read] }]
      responses:
        "200":
          description: List of sites
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Site" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

    post:
      operationId: createSite
      summary: Create a site
      description: |
        Add a new site to your account. Returns a `validation_token` used to verify
        site ownership. Unverified sites are capped at 10 URLs per boost.

        To verify ownership, use one of two methods:

        **Method 1 — HTML file:** Create a file named `{validation_token}.html` at the root of
        your site (e.g. `https://www.example.com/{validation_token}.html`). It must return HTTP 200.

        **Method 2 — Meta tag:** Add `<meta name="cacheboost-token" content="{validation_token}">`
        to the `<head>` of your homepage.

        Then trigger the check from the dashboard. See the [Allow traffic](/guides/allow-traffic) guide.
      tags: [Sites]
      security: [{ bearerAuth: [sites:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  example: "www.example.com"
                  description: Domain without scheme or trailing slash. The `https://` prefix is stripped automatically if provided.
      responses:
        "201":
          description: Site created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SiteCreated" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }

  /sites/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: Site ID

    get:
      operationId: getSite
      summary: Get a site
      description: Retrieve a single site by ID.
      tags: [Sites]
      security: [{ bearerAuth: [sites:read] }]
      responses:
        "200":
          description: Site
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SiteDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

    delete:
      operationId: deleteSite
      summary: Delete a site
      description: |
        Permanently delete a site and all associated sitemaps, boosts, and run history.
        This action is irreversible.
      tags: [Sites]
      security: [{ bearerAuth: [sites:write] }]
      responses:
        "204": { description: Site deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /sites/{id}/validate:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: Site ID

    post:
      operationId: validateSite
      summary: Validate site ownership
      description: |
        Triggers an ownership check for a site. CacheBoost makes an HTTP request
        to the site and looks for the `validation_token` using one of two methods:

        **Method 1 — HTML file:** A file named `{validation_token}.html` must be
        accessible at the site root (e.g. `https://www.example.com/{validation_token}.html`)
        and return HTTP 200.

        **Method 2 — Meta tag:** The homepage must contain
        `<meta name="cacheboost-token" content="{validation_token}">` in its `<head>`.

        On success, the site is marked as validated and `validated: true` is returned.
        If the token is not found, `validated: false` is returned with HTTP 422.

        Returns immediately without polling — the check is synchronous.
      tags: [Sites]
      security: [{ bearerAuth: [sites:write] }]
      responses:
        "200":
          description: Validation succeeded (or site was already validated)
          content:
            application/json:
              schema:
                type: object
                properties:
                  validated:
                    type: boolean
                    example: true
                  already_validated:
                    type: boolean
                    description: Present and `true` when the site was already validated before this call.
                    example: true
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422":
          description: Token not found on the site
          content:
            application/json:
              schema:
                type: object
                properties:
                  validated: { type: boolean, example: false }
                  error:     { type: string,  example: "Validation token not found on the site." }

  # ── Sitemaps ───────────────────────────────────────────────────────────────

  /sites/{id}/sitemaps:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: Site ID

    get:
      operationId: listSitemaps
      summary: List sitemaps
      description: Returns all top-level sitemaps for a site.
      tags: [Sitemaps]
      security: [{ bearerAuth: [sites:read] }]
      responses:
        "200":
          description: List of sitemaps
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Sitemap" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

    post:
      operationId: createSitemap
      summary: Add a sitemap
      description: |
        Add a sitemap URL to a site. CacheBoost downloads and parses it automatically
        in the background. Sitemap indexes are supported — child sitemaps are followed
        automatically.
      tags: [Sitemaps]
      security: [{ bearerAuth: [sites:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  example: "https://www.example.com/sitemap.xml"
                  description: Full URL of the sitemap. Must belong to the site's domain.
      responses:
        "201":
          description: Sitemap added
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Sitemap" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Sitemap URL already exists for this site
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }

  /sitemaps/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: Sitemap ID

    delete:
      operationId: deleteSitemap
      summary: Delete a sitemap
      description: Remove a sitemap and all its extracted URLs.
      tags: [Sitemaps]
      security: [{ bearerAuth: [sites:write] }]
      responses:
        "204": { description: Sitemap deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  # ── Boosts ─────────────────────────────────────────────────────────────────

  /boosts:
    get:
      operationId: listBoosts
      summary: List boosts
      description: Returns all boosts belonging to the authenticated user.
      tags: [Boosts]
      security: [{ bearerAuth: [boosts:read] }]
      responses:
        "200":
          description: List of boosts
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Boost" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

    post:
      operationId: createBoost
      summary: Create a boost
      description: Create a new cache warming campaign.
      tags: [Boosts]
      security: [{ bearerAuth: [boosts:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, site_id, source_type, region]
              properties:
                name:
                  type: string
                  example: "Daily warm"
                site_id:
                  type: integer
                  example: 42
                source_type:
                  type: string
                  enum: [sitemap, csv]
                  example: sitemap
                region:
                  type: array
                  items: { type: string, enum: [fr, us, eu, as] }
                  example: ["fr"]
                  description: Target regions for warming.
                sitemap_ids:
                  type: array
                  items: { type: integer }
                  description: Required when `source_type` is `sitemap`.
                source_url:
                  type: string
                  description: CSV file URL. Required when `source_type` is `csv`.
                url_root:
                  type: string
                  default: "/"
                  description: Only warm URLs that start with this path.
                exclude_ext:
                  type: array
                  items: { type: string }
                  example: ["jpg", "png", "css", "js"]
                  description: File extensions to skip.
                variations:
                  type: object
                  description: Warming variations. Each combination counts as one call per URL.
                  properties:
                    user_agents:
                      type: array
                      items: { type: string, enum: [mobile, desktop, bot, ai_crawler] }
                    languages:
                      type: array
                      items: { type: string }
                      example: ["fr-FR", "en-US"]
                schedule:
                  type: string
                  example: "0 1 * * *"
                  description: Cron expression (UTC) for automatic scheduling.
                call_limit:
                  type: integer
                  description: Maximum number of calls for this boost.
                ignore_errors:
                  type: boolean
                  default: false
                  description: If true, the boost will not auto-pause on high error rates.
                report_email_enabled:
                  type: boolean
                  default: true
                  description: If true, an email report is sent when a run completes.
      responses:
        "201":
          description: Boost created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Boost" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }

  /boosts/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: Boost ID

    get:
      operationId: getBoost
      summary: Get a boost
      description: Retrieve a single boost by ID.
      tags: [Boosts]
      security: [{ bearerAuth: [boosts:read] }]
      responses:
        "200":
          description: Boost
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Boost" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

    put:
      operationId: updateBoost
      summary: Update a boost
      description: |
        Update a boost's configuration. All fields are optional — only include what
        you want to change. Returns `409` if the boost is currently running.
      tags: [Boosts]
      security: [{ bearerAuth: [boosts:write] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:                 { type: string }
                region:               { type: array, items: { type: string } }
                url_root:             { type: string }
                exclude_ext:          { type: array, items: { type: string } }
                variations:           { type: object }
                sitemap_ids:          { type: array, items: { type: integer } }
                content_types:        { type: array, items: { type: string } }
                schedule:             { type: string, nullable: true }
                call_limit:           { type: integer, nullable: true }
                ignore_errors:        { type: boolean }
                report_email_enabled: { type: boolean }
      responses:
        "200":
          description: Boost updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Boost" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Cannot update a running boost
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }

    delete:
      operationId: deleteBoost
      summary: Delete a boost
      description: Permanently delete a boost and all its run history.
      tags: [Boosts]
      security: [{ bearerAuth: [boosts:write] }]
      responses:
        "204": { description: Boost deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /boosts/{id}/run:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: Boost ID

    post:
      operationId: runBoost
      summary: Trigger a run
      description: |
        Manually trigger a boost run immediately. The response is `202 Accepted` —
        the run is queued and processed asynchronously. Poll `GET /runs/{run_id}` to
        track progress. Returns `409` if a run is already in progress.
      tags: [Boosts]
      security: [{ bearerAuth: [boosts:write] }]
      responses:
        "202":
          description: Run queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  run_id: { type: integer, example: 88 }
                  status: { type: string,  example: "pending" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Boost is already running
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  # ── Runs ───────────────────────────────────────────────────────────────────

  /runs:
    get:
      operationId: listRuns
      summary: List runs
      description: Returns run history across all boosts, newest first.
      tags: [Runs]
      security: [{ bearerAuth: [runs:read] }]
      parameters:
        - name: boost_id
          in: query
          schema: { type: integer }
          description: Filter by boost ID.
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
          description: Number of results to return.
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
          description: Pagination offset.
      responses:
        "200":
          description: Paginated list of runs
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Run" }
                  limit:  { type: integer }
                  offset: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /runs/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
        description: Run ID

    get:
      operationId: getRun
      summary: Get a run
      description: Retrieve a single run by ID, including summary statistics.
      tags: [Runs]
      security: [{ bearerAuth: [runs:read] }]
      responses:
        "200":
          description: Run
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RunDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
