{
  "openapi": "3.1.0",
  "info": {
    "title": "PageRadar API v1",
    "version": "1.0.0",
    "description": "Programmatic access to PageRadar monitoring. Hand-written spec, contract-tested against the registered routes in CI (tests/Feature/Api/V1/OpenApiContractTest.php).\n\n**Auth**: Bearer token (create one at /api-access). Tokens carry granular scopes; each operation lists its scope in `x-required-scope`. Legacy `['*']` tokens are READ-ONLY on v1.\n\n**Errors**: RFC 9457 problem+json (`type`, `title`, `status`, `detail`, `code`, `request_id`, optional `errors`). Every response echoes `X-Request-Id`.\n\n**Rate limits**: 100 req/min overall, plus per-class: reads 100/min, writes 30/min, check-runs 6/min. 429 bodies carry `code: rate_limited` and `Retry-After`. On-demand checks also draw from a per-day budget (429 `run_budget_exhausted`).\n\n**Idempotency**: POST create/run endpoints honor an optional `Idempotency-Key` header (replay of 201/202 for 24h; different payload on the same key is a 422 `idempotency_key_conflict`; a concurrent first use is a 409 `idempotency_in_flight`).\n\n**Documentation**: https://pageradar.io/developers/docs is the single doc page for the whole API (v1 + the legacy Reddit endpoints).\n\n**Run tracking**: there are no run ids. `POST .../check` answers `202 queued`; poll `GET /v1/html-monitors/{monitor}` (`last_checked_at` advances, freshness counters move) and the alert lists."
  },
  "servers": [
    {
      "url": "https://pageradar.io/api"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "summary": "This document (public, no auth)",
        "security": [],
        "responses": {
          "200": {
            "description": "The OpenAPI 3.1 spec.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/docs": {
      "get": {
        "operationId": "getDocs",
        "summary": "Redirects to the human + agent documentation page",
        "description": "301 to /developers/docs, the single documentation page for the whole API (it renders this reference alongside the legacy Reddit endpoints). The machine-readable contract is /v1/openapi.json.",
        "security": [],
        "responses": {
          "301": {
            "description": "Redirect to /developers/docs."
          }
        }
      }
    },
    "/v1/user": {
      "get": {
        "operationId": "getUser",
        "summary": "The authenticated account",
        "x-required-scope": "account:read",
        "responses": {
          "200": {
            "description": "Account identity.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string",
                          "format": "email"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          }
        }
      }
    },
    "/v1/sites": {
      "get": {
        "operationId": "listSites",
        "summary": "List your sites (cursor-paginated)",
        "x-required-scope": "sites:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/perPage"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Sites newest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteCollection"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "operationId": "createSite",
        "summary": "Create a site (idempotent per normalized domain)",
        "x-required-scope": "sites:write",
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "domain"
                ],
                "properties": {
                  "domain": {
                    "type": "string",
                    "maxLength": 255,
                    "description": "Domain to group monitors under; normalized server-side (scheme/www stripped)."
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 255
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Site created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteItem"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInFlight"
          }
        },
        "description": "Domains are normalized before storage (scheme and www stripped), so https://www.example.com/ and example.com are the same site. A domain you already own is REFUSED with 422 validation_error — it does not return the existing site. Adding an already-monitored URL to a monitor endpoint behaves differently: it is reported in skipped_duplicates and costs nothing."
      }
    },
    "/v1/sites/{site}": {
      "get": {
        "operationId": "getSite",
        "summary": "One of your sites",
        "x-required-scope": "sites:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/siteId"
          }
        ],
        "responses": {
          "200": {
            "description": "The site.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteItem"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "operationId": "deleteSite",
        "summary": "Permanently delete a site and EVERYTHING monitored under it",
        "description": "IRREVERSIBLE and the widest-reaching call in this API: deletes the site and, by cascade, every monitor of every type it holds (HTML, status code, CWV urls, sitemap, robots.txt, keywords, screenshots, affiliate trackers) together with all of their snapshots, results and alerts. Requires the dedicated `sites:delete` scope, separate from both `sites:write` and `monitors:delete`. `data.deleted` reports how many rows of each type went with it and `meta.quota` the freed capacity per monitor type. A second call on the same id is a 404, so retries are safe.",
        "x-required-scope": "sites:delete",
        "parameters": [
          {
            "$ref": "#/components/parameters/siteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted, with a per-type breakdown of what was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedSite"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/sites/{site}/html-monitors": {
      "get": {
        "operationId": "listHtmlMonitors",
        "summary": "List a site's HTML monitors (cursor-paginated)",
        "x-required-scope": "monitors:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/siteId"
          },
          {
            "$ref": "#/components/parameters/perPage"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Monitors newest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HtmlMonitorCollection"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "operationId": "createHtmlMonitors",
        "summary": "Create HTML monitors (multi-status; incl. freshness / stuck-content mode)",
        "description": "Creates one monitor per URL, always active (they count against your plan quota). Multi-status: the response lists every URL as created / skipped_duplicates / quota_exceeded — 201 when at least one was created, 200 when everything was a duplicate no-op. Zero capacity for genuinely new URLs is a 422 quota problem. The first check of each created monitor is queued automatically.\n\nFreshness mode (detect a page whose content links stopped changing): set freshness_check_enabled with freshness_threshold_hours (daily checks need >= 24; hourly checks accept lower). Freshness needs >= 3 checks before it can flag anything — poll the monitor's freshness block, do not expect an immediate alert.",
        "x-required-scope": "monitors:write",
        "parameters": [
          {
            "$ref": "#/components/parameters/siteId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateHtmlMonitorsRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "At least one monitor created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateHtmlMonitorsResponse"
                }
              }
            }
          },
          "200": {
            "description": "All URLs were already monitored (no-op).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateHtmlMonitorsResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/QuotaOrValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/QuotaLocked"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInFlight"
          }
        }
      }
    },
    "/v1/html-monitors/{monitor}": {
      "get": {
        "operationId": "getHtmlMonitor",
        "summary": "One monitor, incl. live freshness state",
        "description": "Poll target after POST .../check: last_checked_at advances when the queued check completes; freshness.unchanged_checks / freshness.frozen_since expose the stuck-content state machine.",
        "x-required-scope": "monitors:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          }
        ],
        "responses": {
          "200": {
            "description": "The monitor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HtmlMonitorItem"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateHtmlMonitor",
        "summary": "Update config / activation (quota-gated)",
        "description": "Partial update. `url` is immutable (delete + recreate to point elsewhere). Activating (is_active false→true) consumes a quota slot atomically and 422s at capacity; deactivation always succeeds. Freshness coherence is enforced against the EFFECTIVE post-change state (daily checks need threshold >= 24h). When a slot was consumed, meta.quota reports the new usage.",
        "x-required-scope": "monitors:write",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateHtmlMonitorRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated monitor (meta.quota present when activation consumed a slot).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HtmlMonitorItem"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/QuotaOrValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/QuotaLocked"
          }
        }
      },
      "delete": {
        "operationId": "deleteHtmlMonitor",
        "summary": "Permanently delete a HTML monitor",
        "description": "IRREVERSIBLE. Deletes the monitor and, by cascade, every snapshot, content-change alert and freshness alert it owns. Requires the dedicated `monitors:delete` scope — a token with only `monitors:write` gets 403, so an agent can be trusted to configure monitors without being able to destroy their history. A second call on the same id is a 404 (already gone), so retries are safe. meta.quota reports the freed capacity.",
        "x-required-scope": "monitors:delete",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted; meta.quota carries the post-deletion usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedMonitor"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/html-monitors/{monitor}/alerts": {
      "get": {
        "operationId": "listHtmlAlerts",
        "summary": "Content-change alerts (cursor-paginated)",
        "description": "old_value/new_value are clipped to 2000 characters (values_truncated flags clipping).",
        "x-required-scope": "alerts:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          },
          {
            "$ref": "#/components/parameters/perPage"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Alerts newest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HtmlAlertCollection"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/html-monitors/{monitor}/freshness-alerts": {
      "get": {
        "operationId": "listFreshnessAlerts",
        "summary": "Stuck-content (freshness) alerts (cursor-paginated)",
        "x-required-scope": "alerts:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          },
          {
            "$ref": "#/components/parameters/perPage"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Freshness alerts newest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FreshnessAlertCollection"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/html-monitors/{monitor}/check": {
      "post": {
        "operationId": "runHtmlMonitorCheck",
        "summary": "Queue an on-demand check (202 + poll; no run id)",
        "description": "Queues one check now. 202 `queued` on success — then poll GET /v1/html-monitors/{monitor} until last_checked_at advances. 200 `not_run` with reason `inactive` (activate first) or `already_queued` (a check is in flight; retry_after_seconds is the worst-case wait). Over plan quota is a 422 quota problem; the per-day run budget exhausting is a 429 `run_budget_exhausted` with Retry-After. Burst limit: 6/min. Do NOT poll by re-calling this endpoint — poll the monitor.",
        "x-required-scope": "checks:run",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "responses": {
          "202": {
            "description": "Check queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckOutcome"
                }
              }
            }
          },
          "200": {
            "description": "Explicit no-op (reason: inactive | already_queued).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckOutcome"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInFlight"
          },
          "422": {
            "$ref": "#/components/responses/QuotaOrValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitedOrBudget"
          }
        }
      }
    },
    "/v1/sites/{site}/status-monitors": {
      "get": {
        "operationId": "listStatusMonitors",
        "summary": "List a site's HTTP status monitors (cursor-paginated)",
        "x-required-scope": "monitors:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/siteId"
          },
          {
            "$ref": "#/components/parameters/perPage"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Monitors newest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusCodeMonitorCollection"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "operationId": "createStatusMonitors",
        "summary": "Create HTTP status monitors (multi-status)",
        "description": "Creates one monitor per URL, always active (they count against the status-monitor quota). Multi-status response: created / skipped_duplicates / quota_exceeded per URL — 201 when anything was created, 200 when all duplicates, 422 quota problem when nothing fits. The first check queues automatically. Set send_confirmation_email: true to also receive the creation summary email (off by default for API calls).",
        "x-required-scope": "monitors:write",
        "parameters": [
          {
            "$ref": "#/components/parameters/siteId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateStatusMonitorsRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "At least one monitor created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateStatusMonitorsResponse"
                }
              }
            }
          },
          "200": {
            "description": "All URLs were already monitored (no-op).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateStatusMonitorsResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/QuotaOrValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/QuotaLocked"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInFlight"
          }
        }
      }
    },
    "/v1/status-monitors/{monitor}": {
      "get": {
        "operationId": "getStatusMonitor",
        "summary": "One status monitor with its current/previous codes",
        "description": "Poll target after POST .../check: last_checked_at and current_status_code advance when the queued check completes.",
        "x-required-scope": "monitors:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          }
        ],
        "responses": {
          "200": {
            "description": "The monitor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusCodeMonitorItem"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateStatusMonitor",
        "summary": "Update config / activation (quota-gated)",
        "description": "Partial update. `url` is immutable (delete + recreate to point elsewhere). Activating (is_active false→true) consumes a quota slot atomically and 422s at capacity; deactivation always succeeds. When a slot was consumed, meta.quota reports the new usage.",
        "x-required-scope": "monitors:write",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateStatusMonitorRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated monitor (meta.quota present when activation consumed a slot).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusCodeMonitorItem"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/QuotaOrValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/QuotaLocked"
          }
        }
      },
      "delete": {
        "operationId": "deleteStatusMonitor",
        "summary": "Permanently delete a status-code monitor",
        "description": "IRREVERSIBLE. Deletes the monitor and, by cascade, every status-change alert it owns. Requires the dedicated `monitors:delete` scope — a token with only `monitors:write` gets 403, so an agent can be trusted to configure monitors without being able to destroy their history. A second call on the same id is a 404 (already gone), so retries are safe. meta.quota reports the freed capacity.",
        "x-required-scope": "monitors:delete",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted; meta.quota carries the post-deletion usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedMonitor"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/status-monitors/{monitor}/alerts": {
      "get": {
        "operationId": "listStatusAlerts",
        "summary": "Status-change alerts (cursor-paginated)",
        "x-required-scope": "alerts:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          },
          {
            "$ref": "#/components/parameters/perPage"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Alerts newest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusCodeAlertCollection"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/status-monitors/{monitor}/check": {
      "post": {
        "operationId": "runStatusMonitorCheck",
        "summary": "Queue an on-demand status check (202 + poll; no run id)",
        "description": "Same semantics and guardrails as the HTML monitor check endpoint: 202 `queued` (poll GET /v1/status-monitors/{monitor} until last_checked_at advances), 200 `not_run` for `inactive` / `already_queued`, 422 quota problem when over the plan limit, 429 `run_budget_exhausted` when the shared daily run budget is spent, burst limit 6/min. Do NOT poll by re-calling this endpoint.",
        "x-required-scope": "checks:run",
        "parameters": [
          {
            "$ref": "#/components/parameters/monitorId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "responses": {
          "202": {
            "description": "Check queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckOutcome"
                }
              }
            }
          },
          "200": {
            "description": "Explicit no-op (reason: inactive | already_queued).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckOutcome"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInFlight"
          },
          "422": {
            "$ref": "#/components/responses/QuotaOrValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitedOrBudget"
          }
        }
      }
    },
    "/v1/sites/{site}/affiliate-trackers": {
      "get": {
        "operationId": "listAffiliateTrackers",
        "summary": "List a site's affiliate redirect trackers",
        "description": "Geo-targeted affiliate link monitoring: each tracker traces one affiliate URL's full redirect chain from ONE country.",
        "x-required-scope": "monitors:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/siteId"
          },
          {
            "$ref": "#/components/parameters/perPage"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Trackers newest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AffiliateTrackerCollection"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "operationId": "createAffiliateTrackers",
        "summary": "Create affiliate redirect trackers (multi-status)",
        "description": "Creates one tracker per URL for ONE country. The dedup identity is the triple (site, url, country): the same link in another country is a new tracker, the same triple is skipped. Multi-status — 201 when anything was created, 200 when everything was a duplicate. Each created tracker queues a check immediately, so meta.credits reports the balance you have left.",
        "x-required-scope": "monitors:write",
        "parameters": [
          {
            "$ref": "#/components/parameters/siteId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAffiliateTrackersRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "At least one tracker created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateAffiliateTrackersResponse"
                }
              }
            }
          },
          "200": {
            "description": "Every URL was already tracked for that country (no-op).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateAffiliateTrackersResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInFlight"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/affiliate-trackers/{tracker}": {
      "get": {
        "operationId": "getAffiliateTracker",
        "summary": "Get one affiliate tracker",
        "x-required-scope": "monitors:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/trackerId"
          }
        ],
        "responses": {
          "200": {
            "description": "The tracker.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AffiliateTrackerItem"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "operationId": "updateAffiliateTracker",
        "summary": "Update a tracker's configuration",
        "description": "Partial update. `initial_url` and `country_code` are immutable (they are the dedup identity, and past checks were run against them). Unknown keys are ignored rather than rejected. alert_settings is merged, not replaced.",
        "x-required-scope": "monitors:write",
        "parameters": [
          {
            "$ref": "#/components/parameters/trackerId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateAffiliateTrackerRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated tracker.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AffiliateTrackerItem"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "deleteAffiliateTracker",
        "summary": "Permanently delete a tracker",
        "description": "IRREVERSIBLE: every executed check (the redirect chains) and every alert cascade with it. Prefer PATCH {is_active:false} to merely pause monitoring — it stops the credit spend and keeps the history. A second call is a 404, so retries are safe.",
        "x-required-scope": "monitors:delete",
        "parameters": [
          {
            "$ref": "#/components/parameters/trackerId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeletedAffiliateTracker"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/affiliate-trackers/{tracker}/checks": {
      "get": {
        "operationId": "listAffiliateChecks",
        "summary": "List a tracker's executed checks (redirect chains)",
        "description": "Each entry is one executed check with the hop-by-hop redirect chain as observed from the tracker's country (bounded — see redirect_chain). Watch final_domain for affiliate link hijacking.",
        "x-required-scope": "results:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/trackerId"
          },
          {
            "$ref": "#/components/parameters/perPage"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Checks newest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AffiliateCheckCollection"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/affiliate-trackers/{tracker}/alerts": {
      "get": {
        "operationId": "listAffiliateAlerts",
        "summary": "List a tracker's alerts",
        "x-required-scope": "alerts:read",
        "parameters": [
          {
            "$ref": "#/components/parameters/trackerId"
          },
          {
            "$ref": "#/components/parameters/perPage"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "200": {
            "description": "Alerts newest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AffiliateAlertCollection"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/affiliate-trackers/{tracker}/check": {
      "post": {
        "operationId": "runAffiliateCheck",
        "summary": "Queue an on-demand check (202 + poll; spends one credit)",
        "description": "Queues one check of the redirect chain from the tracker's country. There is no run id: on 202, poll GET /v1/affiliate-trackers/{tracker} until last_checked_at advances, then read /checks. **This spends one credit.** Refusals: 200 not_run for `inactive` / `already_queued`; 422 quota problem over the plan limit; 429 `run_budget_exhausted` for the shared DAILY budget; 429 `affiliate_credits_exhausted` for the MONTHLY credit allowance — the latter carries `period_end`, because it resets on the 1st rather than in seconds, and it also stops your scheduled checks until then.",
        "x-required-scope": "checks:run",
        "parameters": [
          {
            "$ref": "#/components/parameters/trackerId"
          },
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "responses": {
          "202": {
            "description": "Check queued; one credit will be spent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckOutcome"
                }
              }
            }
          },
          "200": {
            "description": "Explicit no-op (reason: inactive | already_queued).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckOutcome"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInFlight"
          },
          "422": {
            "$ref": "#/components/responses/QuotaOrValidationError"
          },
          "429": {
            "$ref": "#/components/responses/AffiliateCreditsOrRateLimited"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Sanctum personal access token from /api-access. Scopes per operation via x-required-scope. `monitors:delete` is separate from `monitors:write`: deletion is irreversible and cascades, so it must be granted explicitly. A token's scopes are frozen when it is created — tokens minted before `monitors:delete` existed (including ones created with the `manager` or `full` preset) do NOT have it and answer 403 on DELETE; create a new token to get it. At /api-access you can also pick individual scopes instead of a preset, e.g. to give an agent monitors:write without monitors:delete."
      }
    },
    "parameters": {
      "siteId": {
        "name": "site",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        },
        "description": "Site id (404 if not yours)."
      },
      "monitorId": {
        "name": "monitor",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        },
        "description": "HTML monitor id (404 if not yours)."
      },
      "perPage": {
        "name": "per_page",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 25
        }
      },
      "cursor": {
        "name": "cursor",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "Opaque cursor from links.next/links.prev."
      },
      "idempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Optional. 201/202 responses are stored 24h and replayed verbatim for the same (key, payload); replays carry Idempotency-Replayed: true. Same key + different payload: 422. Concurrent first uses: 409."
      },
      "trackerId": {
        "name": "tracker",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        },
        "description": "Affiliate tracker id (404 if not yours)."
      }
    },
    "schemas": {
      "Meta": {
        "type": "object",
        "properties": {
          "request_id": {
            "type": "string",
            "description": "Echoes X-Request-Id (yours, or minted)."
          },
          "quota": {
            "$ref": "#/components/schemas/Quota"
          },
          "budget": {
            "type": "object",
            "properties": {
              "limit": {
                "type": "integer"
              },
              "remaining": {
                "type": "integer"
              }
            }
          },
          "retry_after_seconds": {
            "type": "integer"
          }
        }
      },
      "Quota": {
        "type": "object",
        "properties": {
          "limit": {
            "type": "integer"
          },
          "used": {
            "type": "integer"
          },
          "remaining": {
            "type": "integer"
          }
        }
      },
      "CursorLinks": {
        "type": "object",
        "properties": {
          "first": {
            "type": [
              "string",
              "null"
            ]
          },
          "last": {
            "type": [
              "string",
              "null"
            ]
          },
          "prev": {
            "type": [
              "string",
              "null"
            ]
          },
          "next": {
            "type": [
              "string",
              "null"
            ],
            "description": "Follow until null to enumerate everything."
          }
        }
      },
      "Site": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "domain": {
            "type": "string"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "SiteItem": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Site"
          }
        }
      },
      "SiteCollection": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Site"
            }
          },
          "links": {
            "$ref": "#/components/schemas/CursorLinks"
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "HtmlMonitor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "type": {
            "type": "string",
            "const": "html_change"
          },
          "site_id": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Immutable after creation."
          },
          "is_active": {
            "type": "boolean"
          },
          "check_frequency": {
            "type": "string",
            "enum": [
              "hourly",
              "daily"
            ]
          },
          "freshness": {
            "type": "object",
            "description": "Stuck-content detection state. frozen_since non-null means the page's content links have not changed past the threshold; unchanged_checks counts consecutive unchanged checks (>= 3 required before freezing).",
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "threshold_hours": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "url_pattern": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "PHP regex filtering which content links count (e.g. #^/\\d{4}/#)."
              },
              "frozen_since": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "unchanged_checks": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "last_new_content_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          },
          "last_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Advances when a queued check completes — the poll signal after POST .../check."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "HtmlMonitorItem": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/HtmlMonitor"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "HtmlMonitorCollection": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HtmlMonitor"
            }
          },
          "links": {
            "$ref": "#/components/schemas/CursorLinks"
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "MonitorConfig": {
        "type": "object",
        "properties": {
          "check_frequency": {
            "type": "string",
            "enum": [
              "hourly",
              "daily"
            ],
            "default": "daily"
          },
          "monitor_full_html": {
            "type": "boolean",
            "default": false
          },
          "elements_to_monitor": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "excluded_properties": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "focus_keywords": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 255
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 255
            }
          },
          "email_notifications_enabled": {
            "type": "boolean",
            "default": true
          },
          "slack_notifications_enabled": {
            "type": "boolean",
            "default": false
          },
          "slack_webhook_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "maxLength": 2048
          },
          "freshness_check_enabled": {
            "type": "boolean",
            "default": false
          },
          "freshness_threshold_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 168,
            "description": "Required when freshness_check_enabled. Daily checks require >= 24; hourly checks accept lower (e.g. 12)."
          },
          "freshness_url_pattern": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 255,
            "description": "Valid PHP regex, e.g. #^/\\d{4}/#."
          }
        }
      },
      "CreateHtmlMonitorsRequest": {
        "allOf": [
          {
            "type": "object",
            "required": [
              "urls"
            ],
            "properties": {
              "urls": {
                "type": "array",
                "minItems": 1,
                "maxItems": 20,
                "items": {
                  "type": "string",
                  "format": "uri",
                  "maxLength": 2048
                }
              }
            }
          },
          {
            "$ref": "#/components/schemas/MonitorConfig"
          }
        ],
        "description": "is_active is NOT settable at creation: API-created monitors are always active so they count against quota."
      },
      "UpdateHtmlMonitorRequest": {
        "allOf": [
          {
            "type": "object",
            "properties": {
              "is_active": {
                "type": "boolean",
                "description": "false→true re-runs the atomic quota reservation (422 at capacity)."
              },
              "custom_settings": {
                "type": "object"
              }
            }
          },
          {
            "$ref": "#/components/schemas/MonitorConfig"
          }
        ],
        "description": "All fields optional; unknown fields (incl. url) are ignored."
      },
      "CreateHtmlMonitorsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "created": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/HtmlMonitor"
                }
              },
              "skipped_duplicates": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "URLs already monitored on this site."
              },
              "quota_exceeded": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "URLs that did not fit in the remaining quota."
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "CheckOutcome": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "monitor_id": {
                "type": "integer"
              },
              "status": {
                "type": "string",
                "enum": [
                  "queued",
                  "not_run"
                ]
              },
              "reason": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "inactive",
                  "already_queued",
                  null
                ]
              },
              "detail": {
                "type": "string",
                "description": "Human/LLM-readable next step."
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "HtmlAlert": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "html_monitor_id": {
            "type": "integer"
          },
          "change_type": {
            "type": "string"
          },
          "element_type": {
            "type": "string"
          },
          "element_selector": {
            "type": [
              "string",
              "null"
            ]
          },
          "old_value": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          },
          "new_value": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          },
          "values_truncated": {
            "type": "boolean"
          },
          "severity": {
            "type": "string",
            "enum": [
              "critical",
              "high",
              "medium",
              "low"
            ]
          },
          "focus_keyword_affected": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_read": {
            "type": "boolean"
          },
          "detected_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "HtmlAlertCollection": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HtmlAlert"
            }
          },
          "links": {
            "$ref": "#/components/schemas/CursorLinks"
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "FreshnessAlert": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "html_monitor_id": {
            "type": "integer"
          },
          "type": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "frozen_since": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "frozen_minutes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "content_links_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "is_read": {
            "type": "boolean"
          },
          "detected_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "FreshnessAlertCollection": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FreshnessAlert"
            }
          },
          "links": {
            "$ref": "#/components/schemas/CursorLinks"
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "StatusCodeMonitor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "type": {
            "type": "string",
            "const": "status_code"
          },
          "site_id": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Immutable after creation."
          },
          "is_active": {
            "type": "boolean"
          },
          "check_frequency": {
            "type": "string",
            "enum": [
              "hourly",
              "daily",
              "weekly"
            ]
          },
          "current_status_code": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Last recorded HTTP status; 0 is the reserved sentinel for connection errors. Null until the first check completes."
          },
          "previous_status_code": {
            "type": [
              "integer",
              "null"
            ]
          },
          "notifications": {
            "type": "object",
            "properties": {
              "slack_enabled": {
                "type": "boolean"
              },
              "slack_webhook_url": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "additional_emails": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "string",
                  "format": "email"
                }
              }
            }
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "last_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Advances when a queued check completes — the poll signal after POST .../check."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "StatusCodeMonitorItem": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/StatusCodeMonitor"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "StatusCodeMonitorCollection": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatusCodeMonitor"
            }
          },
          "links": {
            "$ref": "#/components/schemas/CursorLinks"
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "StatusMonitorConfig": {
        "type": "object",
        "properties": {
          "check_frequency": {
            "type": "string",
            "enum": [
              "hourly",
              "daily",
              "weekly"
            ],
            "default": "hourly"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 255
            }
          },
          "slack_notifications_enabled": {
            "type": "boolean",
            "default": false
          },
          "slack_webhook_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "maxLength": 2048
          },
          "additional_notification_emails": {
            "type": [
              "array",
              "null"
            ],
            "maxItems": 2,
            "items": {
              "type": "string",
              "format": "email"
            },
            "description": "Extra recipients for status-change alerts, on top of the account email."
          }
        }
      },
      "CreateStatusMonitorsRequest": {
        "allOf": [
          {
            "type": "object",
            "required": [
              "urls"
            ],
            "properties": {
              "urls": {
                "type": "array",
                "minItems": 1,
                "maxItems": 20,
                "items": {
                  "type": "string",
                  "format": "uri",
                  "maxLength": 2048
                }
              },
              "send_confirmation_email": {
                "type": "boolean",
                "default": false,
                "description": "Opt-in: also email the account a creation summary (the dashboard sends it; API calls stay silent by default)."
              }
            }
          },
          {
            "$ref": "#/components/schemas/StatusMonitorConfig"
          }
        ],
        "description": "is_active is NOT settable at creation: API-created monitors are always active so they count against quota."
      },
      "UpdateStatusMonitorRequest": {
        "allOf": [
          {
            "type": "object",
            "properties": {
              "is_active": {
                "type": "boolean",
                "description": "false→true re-runs the atomic quota reservation (422 at capacity)."
              }
            }
          },
          {
            "$ref": "#/components/schemas/StatusMonitorConfig"
          }
        ],
        "description": "All fields optional; unknown fields (incl. url) are ignored."
      },
      "CreateStatusMonitorsResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "created": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/StatusCodeMonitor"
                }
              },
              "skipped_duplicates": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "URLs already monitored on this site."
              },
              "quota_exceeded": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "URLs that did not fit in the remaining quota."
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "StatusCodeAlert": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "status_code_monitor_id": {
            "type": "integer"
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          },
          "old_status_code": {
            "type": [
              "integer",
              "null"
            ]
          },
          "new_status_code": {
            "type": [
              "integer",
              "null"
            ]
          },
          "change_type": {
            "type": "string",
            "enum": [
              "critical",
              "recovery",
              "redirect",
              "change"
            ],
            "description": "critical: 2xx→4xx/5xx; recovery: 4xx/5xx→2xx; redirect: 2xx→3xx; change: everything else. IMPORTANT: transitions involving status 0 (connection error — a fully unreachable site) are classified `change`, NOT critical/recovery. To detect real downtime, branch on the status codes (new_status_code === 0 || new_status_code >= 400), not on change_type alone."
          },
          "is_read": {
            "type": "boolean"
          },
          "detected_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "StatusCodeAlertCollection": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatusCodeAlert"
            }
          },
          "links": {
            "$ref": "#/components/schemas/CursorLinks"
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details.",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "validation_error",
              "quota_exceeded",
              "quota_locked",
              "unauthenticated",
              "forbidden",
              "insufficient_scope",
              "not_found",
              "rate_limited",
              "run_budget_exhausted",
              "idempotency_key_conflict",
              "idempotency_in_flight",
              "http_error",
              "server_error",
              "affiliate_credits_exhausted"
            ]
          },
          "request_id": {
            "type": "string"
          },
          "errors": {
            "type": "object",
            "description": "code=validation_error: field=>messages. code=quota_exceeded: errors.quota {monitor_type, limit, used, remaining, requested}. code=rate_limited: errors.rate_limit.retry_after_seconds. code=run_budget_exhausted: errors.budget {limit, remaining, retry_after_seconds}."
          }
        }
      },
      "DeletedMonitor": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "description": "The id that no longer exists."
              },
              "type": {
                "type": "string",
                "enum": [
                  "html_change",
                  "status_code"
                ]
              },
              "url": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The URL the deleted monitor watched."
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "DeletedSite": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "domain": {
                "type": "string"
              },
              "deleted": {
                "type": "object",
                "description": "Rows removed per monitor type (counted before the cascade).",
                "additionalProperties": {
                  "type": "integer"
                }
              }
            }
          },
          "meta": {
            "type": "object",
            "properties": {
              "quota": {
                "type": "object",
                "description": "Freed capacity, keyed by monitor type.",
                "additionalProperties": {
                  "$ref": "#/components/schemas/Quota"
                }
              },
              "request_id": {
                "type": "string"
              }
            }
          }
        }
      },
      "AffiliateTracker": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "type": {
            "type": "string",
            "const": "affiliate_redirect"
          },
          "site_id": {
            "type": "integer"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The affiliate link being traced."
          },
          "country_code": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2,
            "description": "ISO 3166-1 alpha-2. A tracker watches ONE country — covering N countries means N trackers, and N credits per check cycle."
          },
          "is_active": {
            "type": "boolean"
          },
          "check_frequency_hours": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Hours between automatic checks (24 = daily, 168 = weekly)."
          },
          "alert_settings": {
            "type": [
              "object",
              "null"
            ],
            "description": "Which transitions raise an alert: redirect_change, final_domain_change, status_error, timeout (always on), blocked_detected."
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "last_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "next_check_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the scheduler will check next. Pushed to the 1st of next month when monthly credits run out."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "AffiliateTrackerItem": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AffiliateTracker"
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "AffiliateTrackerCollection": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AffiliateTracker"
            }
          },
          "links": {
            "$ref": "#/components/schemas/CursorLinks"
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "AffiliateCheck": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "tracker_id": {
            "type": "integer"
          },
          "final_status_code": {
            "type": "integer",
            "description": "Status of the final destination. 0 means the chain could not be completed at all — that is the value to test for a dead affiliate link, alongside >= 400."
          },
          "final_domain": {
            "type": [
              "string",
              "null"
            ],
            "description": "Domain the affiliate link ultimately landed on — the value to watch for hijacking."
          },
          "total_redirects": {
            "type": [
              "integer",
              "null"
            ]
          },
          "total_response_time_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "is_successful": {
            "type": "boolean"
          },
          "error_message": {
            "type": [
              "string",
              "null"
            ]
          },
          "redirect_chain": {
            "type": "array",
            "description": "Each hop as observed FROM the tracker's country. Bounded twice: withheld entirely (empty array) when the stored chain exceeds ~64 KB, and otherwise capped at 25 hops. `redirect_chain_truncated` is true in either case.",
            "items": {
              "type": "object"
            }
          },
          "redirect_chain_truncated": {
            "type": "boolean",
            "description": "True when the chain was withheld for size or clipped at 25 hops — the stored chain has more than you see here."
          },
          "checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "AffiliateCheckCollection": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AffiliateCheck"
            }
          },
          "links": {
            "$ref": "#/components/schemas/CursorLinks"
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "AffiliateAlert": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "tracker_id": {
            "type": "integer"
          },
          "check_id": {
            "type": "integer",
            "description": "The check that produced this alert."
          },
          "alert_type": {
            "type": "string",
            "enum": [
              "status_change",
              "domain_change",
              "redirect_count_change",
              "performance_degradation",
              "potential_block",
              "redirect_path_change"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "critical",
              "high",
              "medium",
              "low"
            ]
          },
          "description": {
            "type": "string"
          },
          "alert_data": {
            "type": "object",
            "description": "Type-specific payload (old/new values)."
          },
          "is_resolved": {
            "type": "boolean"
          },
          "resolved_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "detected_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "AffiliateAlertCollection": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AffiliateAlert"
            }
          },
          "links": {
            "$ref": "#/components/schemas/CursorLinks"
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "CreateAffiliateTrackersRequest": {
        "type": "object",
        "required": [
          "urls",
          "country_code"
        ],
        "properties": {
          "urls": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": {
              "type": "string",
              "format": "uri",
              "maxLength": 2048
            },
            "description": "Capped at 100 — lower than the dashboard's 1000 on purpose: every created tracker queues a check immediately, and one check costs one credit."
          },
          "country_code": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2,
            "description": "ISO 3166-1 alpha-2, from the 169 proxy-supported countries enumerated here. An unsupported code is a 422 — read this enum rather than guessing, since the list is a proxy-network capability, not the ISO list.",
            "enum": [
              "US",
              "GB",
              "FR",
              "ES",
              "DE",
              "IT",
              "CA",
              "AU",
              "BR",
              "MX",
              "IN",
              "JP",
              "KR",
              "SG",
              "NL",
              "CH",
              "SE",
              "NO",
              "DK",
              "FI",
              "AF",
              "DZ",
              "AO",
              "BJ",
              "BW",
              "BF",
              "BI",
              "CM",
              "CF",
              "TD",
              "CG",
              "CI",
              "DJ",
              "EG",
              "GQ",
              "ER",
              "SZ",
              "ET",
              "GA",
              "GM",
              "GH",
              "GN",
              "GW",
              "KE",
              "LS",
              "LR",
              "LY",
              "MG",
              "MW",
              "ML",
              "MR",
              "MA",
              "MZ",
              "NA",
              "NE",
              "NG",
              "RW",
              "SN",
              "SL",
              "SO",
              "ZA",
              "SD",
              "TZ",
              "TG",
              "TN",
              "UG",
              "BS",
              "BZ",
              "CR",
              "CU",
              "DO",
              "SV",
              "GT",
              "HT",
              "HN",
              "JM",
              "NI",
              "PA",
              "TT",
              "AR",
              "BO",
              "CL",
              "CO",
              "EC",
              "FK",
              "GF",
              "GY",
              "PY",
              "PE",
              "SR",
              "UY",
              "VE",
              "BD",
              "BT",
              "BN",
              "KH",
              "CN",
              "ID",
              "KZ",
              "KG",
              "LA",
              "MY",
              "MN",
              "MM",
              "NP",
              "PK",
              "PH",
              "LK",
              "TJ",
              "TH",
              "TL",
              "TM",
              "UZ",
              "VN",
              "HK",
              "TW",
              "AL",
              "AM",
              "AT",
              "AZ",
              "BY",
              "BE",
              "BA",
              "BG",
              "HR",
              "CY",
              "CZ",
              "EE",
              "GE",
              "GR",
              "HU",
              "IS",
              "IE",
              "LV",
              "LT",
              "LU",
              "MT",
              "MD",
              "ME",
              "MK",
              "PL",
              "PT",
              "RO",
              "RU",
              "RS",
              "SK",
              "SI",
              "TR",
              "UA",
              "IR",
              "IQ",
              "IL",
              "JO",
              "KW",
              "LB",
              "OM",
              "PS",
              "QA",
              "SA",
              "SY",
              "AE",
              "YE",
              "FJ",
              "NC",
              "NZ",
              "PG",
              "VU",
              "BM",
              "PR"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 255,
            "description": "Base name. With several URLs it is suffixed ' #1', ' #2'… Defaults to the link's host."
          },
          "check_frequency_hours": {
            "type": "integer",
            "enum": [
              24,
              48,
              168
            ],
            "default": 24
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "maxLength": 255
            }
          },
          "alert_settings": {
            "type": "object",
            "description": "All default to true. `timeout` is forced on and cannot be disabled.",
            "properties": {
              "redirect_change": {
                "type": "boolean"
              },
              "final_domain_change": {
                "type": "boolean"
              },
              "status_error": {
                "type": "boolean"
              },
              "blocked_detected": {
                "type": "boolean"
              }
            }
          }
        },
        "description": "is_active is not caller-settable: API-created trackers are active."
      },
      "CreateAffiliateTrackersResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "created": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AffiliateTracker"
                }
              },
              "skipped_duplicates": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "URLs already tracked for that site AND country."
              }
            }
          },
          "meta": {
            "type": "object",
            "properties": {
              "credits": {
                "type": "object",
                "description": "Monthly credit balance. NOTE: credits are debited when a check RUNS, not when a tracker is created — so `used` here does not yet include the checks this call just queued. `checks_queued` says how many are about to be spent.",
                "properties": {
                  "limit": {
                    "type": "integer"
                  },
                  "used": {
                    "type": "integer"
                  },
                  "remaining": {
                    "type": "integer"
                  },
                  "period_end": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When the monthly allowance resets."
                  },
                  "checks_queued": {
                    "type": "integer",
                    "description": "Checks queued by this call — each will spend one credit shortly."
                  }
                }
              },
              "request_id": {
                "type": "string"
              }
            }
          }
        }
      },
      "UpdateAffiliateTrackerRequest": {
        "type": "object",
        "description": "All fields optional. `initial_url` and `country_code` are absent on purpose: with the site they form the tracker's identity and are not editable — delete and recreate to watch another link or another country.",
        "properties": {
          "is_active": {
            "type": "boolean",
            "description": "false stops the checks (and the credit spend) while keeping the history — the reversible alternative to DELETE."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 255
          },
          "check_frequency_hours": {
            "type": "integer",
            "enum": [
              24,
              48,
              168
            ]
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "alert_settings": {
            "type": "object",
            "description": "MERGED onto the current settings, so a partial PATCH keeps the toggles it does not mention. `timeout` is forced back on.",
            "properties": {
              "redirect_change": {
                "type": "boolean"
              },
              "final_domain_change": {
                "type": "boolean"
              },
              "status_error": {
                "type": "boolean"
              },
              "blocked_detected": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "DeletedAffiliateTracker": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "type": {
                "type": "string",
                "const": "affiliate_redirect"
              },
              "url": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      }
    },
    "responses": {
      "Unauthenticated": {
        "description": "Missing/invalid token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Token lacks the required scope (code: insufficient_scope). Legacy ['*'] tokens are read-only on v1.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not yours or nonexistent — indistinguishable by design.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ValidationError": {
        "description": "code: validation_error with field errors.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "QuotaOrValidationError": {
        "description": "code: validation_error (field errors) or quota_exceeded (errors.quota carries limit/used/remaining).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "QuotaLocked": {
        "description": "code: quota_locked — transient contention on the quota lock; retry shortly.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "RateLimited": {
        "description": "code: rate_limited — per-class burst limit. Honor Retry-After.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "RateLimitedOrBudget": {
        "description": "code: rate_limited (burst, 6/min) or run_budget_exhausted (daily budget; errors.budget.retry_after_seconds). Honor Retry-After — do not loop.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "IdempotencyInFlight": {
        "description": "code: idempotency_in_flight — the first request with this key is still executing; retry shortly.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "AffiliateCreditsOrRateLimited": {
        "description": "code: rate_limited (burst, 6/min), run_budget_exhausted (shared daily budget), or affiliate_credits_exhausted (MONTHLY allowance; errors.credits carries limit, remaining and period_end). Only the last one means 'come back next month' — the other two are minutes and a day.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    }
  }
}
