{
  "openapi": "3.0.3",
  "info": {
    "title": "CodaURL API",
    "description": "URL monitoring API for AI agents and developers. User-session Bearer tokens are required for authenticated non-monitor routes. Authenticated requests share an account-wide budget and may also be rate-limited by a verified web, mobile, agent, or unverified channel; 429 rate_limited includes Retry-After in seconds. Monitor mutations return 503 billing_projection_pending with Retry-After while plan projection is incomplete. JSON request bodies parsed by the shared API helper are limited to 64 KiB. Instant-check, auth and Stripe Checkout bodies are limited to 4 KiB (auth routes use the flat {error, message} envelope); Stripe webhook bodies are limited to 256 KiB. Oversized bodies return 413 with error code request_too_large.",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.codaurl.com/api/v1",
      "description": "Production API Server"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "paths": {
    "/monitors": {
      "get": {
        "summary": "List all monitors",
        "operationId": "listMonitors",
        "responses": {
          "200": {
            "description": "Successful monitor list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitors": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Monitor"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Add new monitor",
        "operationId": "addMonitor",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url"],
                "properties": {
                  "url": { "type": "string", "example": "https://my-service.com/health" },
                  "name": { "type": "string", "nullable": true, "description": "Optional user-defined monitor name. Leading and trailing whitespace is trimmed; the result must be at most 80 characters. An empty result clears the name." },
                  "expected_status": { "$ref": "#/components/schemas/ExpectedStatusInput" },
                  "request_headers": { "$ref": "#/components/schemas/RequestHeadersInput" },
                  "keyword": { "type": "string", "minLength": 1, "maxLength": 256, "description": "Pro/Enterprise only. Case-sensitive UTF-8 substring tested against the response body. Maximum 256 UTF-8 bytes." },
                  "keyword_mode": { "type": "string", "enum": ["contains", "not_contains"], "default": "contains", "description": "Requires keyword. Keyword checks use GET and read at most 64 KiB of the decoded body." },
                  "latency_baseline_enabled": { "type": "boolean", "default": false, "description": "Paid-tier opt-in response latency warnings against an automatic baseline learned from up to the last 7 days of samples; a new monitor is typically ready after about 2-3 hours of data. 3 consecutive sampled scheduled results above the READY upper bound warn; 2 at or below recover. Evaluation reuses stored samples at a minimum five-minute cadence, or the slower check/storage interval; with five-minute samples, sustained warnings take about 10-15 minutes and recovery 5-10 minutes. Availability checks keep their configured interval. Free receives plan_required." },
                  "check_interval_seconds": {
                    "type": "integer",
                    "enum": [60, 300, 900, 1800, 3600],
                    "description": "Free: 300|900|1800|3600 (default 300). Pro/Enterprise: 60|300|900|1800|3600 (default 60).",
                    "default": 300
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Monitor created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitor": { "$ref": "#/components/schemas/Monitor" },
                    "already_exists": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Monitor created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitor": { "$ref": "#/components/schemas/Monitor" },
                    "already_exists": { "type": "boolean", "enum": [false] }
                  }
                }
              }
            }
          },
          "409": { "description": "Monitor limit reached (monitor_limit_reached), or monitor entitlement changed during the request (monitor_definition_conflict); retry the request" },
          "503": { "description": "tenant_provisioning when the tenant monitor limit is not projected; retry after entitlement is applied" },
          "413": { "description": "Request body exceeds 64 KiB (request_too_large)" },
          "429": { "description": "Replacement budget exhausted (monitor_creation_budget_exhausted). Free: 10 successful new monitors per rolling 24 hours or 30 per rolling 30 days. Pro: 30 per rolling 24 hours or 100 per rolling 30 days after the first 25 creations. Retry-After is in seconds. Deletes and requests for an existing URL do not consume the budget." }
        }
      }
    },
    "/monitors/{id}": {
      "get": {
        "summary": "Get one monitor",
        "operationId": "getMonitor",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Monitor detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitor": { "$ref": "#/components/schemas/Monitor" }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Monitor not found for this user"
          }
        }
      },
      "patch": {
        "summary": "Edit monitor settings",
        "operationId": "editMonitor",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string", "nullable": true, "description": "Optional user-defined monitor name. Leading and trailing whitespace is trimmed; the result must be at most 80 characters. An empty result clears the name." },
                  "expected_status": { "$ref": "#/components/schemas/ExpectedStatusInput" },
                  "request_headers": { "$ref": "#/components/schemas/RequestHeadersInput" },
                  "keyword": { "type": "string", "nullable": true, "minLength": 1, "maxLength": 256, "description": "Replace the paid body keyword; null clears it. Omission preserves it." },
                  "keyword_mode": { "type": "string", "enum": ["contains", "not_contains"], "description": "Set the paid keyword condition. Requires a stored or supplied keyword." },
                  "latency_baseline_enabled": { "type": "boolean", "description": "Paid-tier opt-in response latency warnings against an automatic baseline learned from up to the last 7 days of samples; a new monitor is typically ready after about 2-3 hours of data. 3 consecutive sampled scheduled results above the READY upper bound warn; 2 at or below recover. Evaluation reuses stored samples at a minimum five-minute cadence, or the slower check/storage interval; with five-minute samples, sustained warnings take about 10-15 minutes and recovery 5-10 minutes. Availability checks keep their configured interval. Free receives plan_required." },
                  "check_interval_seconds": {
                    "type": "integer",
                    "enum": [60, 300, 900, 1800, 3600],
                    "description": "Must be allowed for the caller's tier. The new interval applies from the next minute that matches the monitor's schedule phase."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Monitor updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitor": { "$ref": "#/components/schemas/Monitor" }
                  }
                }
              }
            }
          },
          "409": { "description": "monitor_definition_conflict when concurrent edits exhausted retries or monitor entitlement changed; resend the request" }
        }
      },
      "delete": {
        "summary": "Delete monitor",
        "operationId": "deleteMonitor",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Monitor deleted"
          }
        }
      }
    },
    "/monitors/{id}/baseline-preview": {
      "post": {
        "summary": "Preview automatic response baseline without sending notifications",
        "operationId": "previewAutomaticBaseline",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "description": "Empty object."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Baseline preview. This operation does not create notification history or send notifications.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["id", "algorithm_version", "status", "window", "coverage", "model", "event_count", "events", "series"],
                  "properties": {
                    "id": { "type": "string" },
                    "algorithm_version": { "type": "string", "enum": ["pooled-7d-mad-v2"] },
                    "status": { "type": "string", "enum": ["LEARNING", "READY"] },
                    "window": { "type": "object", "description": "Seven days ending at the start of the current UTC hour.", "required": ["start_at", "end_at"], "properties": {
                      "start_at": { "type": "string", "format": "date-time" },
                      "end_at": { "type": "string", "format": "date-time" }
                    } },
                    "coverage": { "type": "object", "required": ["observed_samples", "missing_days"], "properties": {
                      "observed_samples": { "type": "integer", "description": "Five-minute latency samples found in the window." },
                      "missing_days": { "type": "integer", "description": "Days of the seven-day window with no samples." }
                    } },
                    "model": { "type": "object", "required": ["version", "status", "sample_count", "median_ms", "upper_bound_ms"], "properties": {
                      "version": { "type": "string", "enum": ["pooled-7d-mad-v2"] },
                      "status": { "type": "string", "enum": ["LEARNING", "READY"] },
                      "sample_count": { "type": "integer" },
                      "median_ms": { "type": "integer", "nullable": true },
                      "upper_bound_ms": { "type": "integer", "nullable": true }
                    } },
                    "event_count": { "type": "integer" },
                    "events": { "type": "array", "items": { "$ref": "#/components/schemas/AutomaticBaselinePreviewPoint" } },
                    "events_truncated": { "type": "boolean" },
                    "series": { "type": "array", "items": { "$ref": "#/components/schemas/AutomaticBaselinePreviewPoint" } },
                    "series_truncated": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid preview request" },
          "403": { "description": "plan_required: automatic baselines require Pro or Enterprise" },
          "404": { "description": "Monitor not found for this user" }
        }
      }
    },
    "/monitors/{id}/logs": {
      "get": {
        "summary": "Get monitor check logs or latency rollup",
        "description": "No range: recent raw check logs (limit, default 50, max 500). range=1h: raw latency logs from the last 60 minutes (checked_at >= now - 1h). range=24h|7d|30d: rollup buckets (30d requires Pro/Enterprise).",
        "operationId": "getMonitorLogs",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["1h", "24h", "7d", "30d"] }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 50 },
            "description": "Only applies when range is not set."
          }
        ],
        "responses": {
          "200": {
            "description": "Logs or rollup buckets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["id"],
                  "properties": {
                    "id": { "type": "string" },
                    "range": { "type": "string", "enum": ["1h", "24h", "7d", "30d"] },
                    "granularity": { "type": "string" },
                    "logs": { "type": "array", "items": { "type": "object" } },
                    "buckets": { "type": "array", "items": { "type": "object" } }
                  }
                }
              }
            }
          },
          "403": {
            "description": "plan_required - 30d range needs Pro or Enterprise"
          },
          "404": {
            "description": "Monitor not found for this user"
          },
          "429": {
            "description": "log_query_budget_exhausted, log_query_daily_budget_exhausted (raw limit reads only; Retry-After is the time to the next UTC midnight), or rate_limited; retry after the Retry-After response header"
          },
          "503": {
            "description": "logs_unavailable when raw check-log storage cannot be read, or overload_protection from the global Worker request guard. Retry after the Retry-After response header; logs_unavailable uses 5 seconds.",
            "headers": {
              "Retry-After": { "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } }
            }
          }
        }
      }
    },
    "/monitors/{id}/pause": {
      "post": {
        "summary": "Pause monitor checks",
        "description": "Pauses the monitor with paused_reason=manual. Pausing an already paused monitor is a no-op that returns 200 and keeps its existing paused_reason. Returns 409 monitor_state_conflict when concurrent status writes keep winning; retry the request.",
        "operationId": "pauseMonitor",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Monitor paused",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "paused": { "type": "boolean" },
                    "monitor": { "$ref": "#/components/schemas/Monitor" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/monitors/{id}/resume": {
      "post": {
        "summary": "Resume a paused monitor",
        "description": "Returns 409 monitor_limit_reached at the active monitor limit, or 409 monitor_state_conflict when concurrent status writes keep winning; retry the request.",
        "operationId": "resumeMonitor",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Monitor resumed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "resumed": { "type": "boolean" },
                    "monitor": { "$ref": "#/components/schemas/Monitor" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/monitors/{id}/mute": {
      "post": {
        "summary": "Mute alerts until a time (checks stay LIVE)",
        "description": "Sets muted_until. UP<->DOWN status changes are still recorded; notification is skipped with check.notification=muted until expiry. Exactly one of until or duration_seconds is required. Max 90 days.",
        "operationId": "muteMonitor",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "until": {
                    "type": "string",
                    "format": "date-time",
                    "description": "ISO 8601 mute end (must be in the future)"
                  },
                  "duration_seconds": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Mute for N seconds from now"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Monitor muted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "muted": { "type": "boolean" },
                    "monitor": { "$ref": "#/components/schemas/Monitor" }
                  }
                }
              }
            }
          },
          "409": { "description": "monitor_definition_conflict after concurrent edits; resend the request" }
        }
      }
    },
    "/monitors/{id}/unmute": {
      "post": {
        "summary": "Clear maintenance mute",
        "operationId": "unmuteMonitor",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Mute cleared",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "muted": { "type": "boolean" },
                    "monitor": { "$ref": "#/components/schemas/Monitor" }
                  }
                }
              }
            }
          },
          "409": { "description": "monitor_definition_conflict after concurrent edits; resend the request" }
        }
      }
    },
    "/monitors/{id}/check": {
      "post": {
        "summary": "Trigger instant HTTP check",
        "description": "A monitor may be checked once per 60 seconds, and each account has a UTC daily budget of 50 immediate checks. Exceeding either limit returns 429 with Retry-After and error code instant_check_cooldown or instant_check_daily_budget_exhausted. A paused monitor returns 409 monitor_paused; resume it first.",
        "operationId": "checkMonitorInstantly",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Check outcome",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitor": { "$ref": "#/components/schemas/Monitor" },
                    "check": {
                      "type": "object",
                      "properties": {
                        "previous_status": { "type": "string", "nullable": true },
                        "status": { "type": "string", "enum": ["UP", "DOWN"] },
                        "status_changed": { "type": "boolean" },
                        "checked_at": { "type": "string", "format": "date-time" },
                        "status_since": { "type": "string", "format": "date-time" },
                        "error_code": { "type": "string", "nullable": true },
                        "keyword_matched": { "type": "boolean", "nullable": true, "description": "Keyword condition verdict. Null when no keyword is configured or HTTP/body could not be evaluated." },
                        "notification": {
                          "type": "string",
                          "enum": ["not_required", "sent", "failed", "muted"],
                          "description": "muted = status change recorded but notify skipped"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "429": {
            "description": "Immediate-check monitor cooldown or account UTC daily budget is exhausted. Retry-After is the next allowed wait in seconds."
          }
        }
      }
    },
    "/monitors/{id}/ssl-check": {
      "post": {
        "summary": "Refresh and save SSL/TLS checks from the mobile app",
        "description": "Mobile app only. Requires a user session and a valid Firebase App Check token for an allowlisted Android or iOS app. API keys and agent clients are not supported. For an owned Pro or Enterprise monitor, observes the live peer certificate and separately validates the TLS connection, saves the result, and returns the updated monitor. Paused monitors are eligible. Shares the immediate-check per-monitor cooldown and account daily budget with the HTTP instant-check endpoint. This manual refresh does not send an SSL expiry notification.",
        "operationId": "refreshMonitorSsl",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "X-Firebase-AppCheck", "in": "header", "required": true, "schema": { "type": "string" }, "description": "Firebase App Check token from the mobile app." }
        ],
        "responses": {
          "200": {
            "description": "Updated monitor with saved SSL/TLS result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["monitor"],
                  "properties": { "monitor": { "$ref": "#/components/schemas/Monitor" } }
                }
              }
            }
          },
          "403": { "description": "Verified mobile app or paid plan required (mobile_app_required or plan_required); agent sessions are denied account-level routes with interactive_session_required" },
          "404": { "description": "Monitor not found for this user" },
          "429": { "description": "Rate limit, shared immediate-check cooldown, or daily budget exhausted. Retry-After is in seconds." }
        }
      }
    },
    "/ai-review/reports": {
      "post": {
        "summary": "Report a private AI review comment from the verified mobile app",
        "description": "Requires a user session and Firebase App Check (mobile_app). Send the monitor ID and generation time of a comment shown to this account. The server records its own safety-checked text; clients cannot upload a comment body. Duplicate submissions return the same receipt. At most 10 new reports per rolling 24 hours. Reports are scheduled for deletion after 30 days or removed on account erasure.",
        "security": [{ "BearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": {
            "type": "object", "additionalProperties": false,
            "required": ["monitor_id", "generated_at", "reason"],
            "properties": {
              "monitor_id": { "type": "string", "pattern": "^[a-zA-Z0-9_-]{1,100}$" },
              "generated_at": { "type": "string", "format": "date-time" },
              "reason": { "type": "string", "enum": ["unsafe", "offensive", "incorrect", "other"] }
            }
          } } }
        },
        "responses": {
          "201": { "description": "Report durably received", "content": { "application/json": { "schema": {
            "type": "object", "required": ["report"], "properties": { "report": {
              "type": "object", "required": ["id", "state"], "properties": {
                "id": { "type": "string" }, "state": { "type": "string", "enum": ["received"] }
              }
            } }
          } } } },
          "200": { "description": "Previously received report; retry did not create a duplicate" },
          "400": { "description": "validation_error; unknown fields are rejected" },
          "401": { "description": "Valid user session required" },
          "403": { "description": "mobile_app_required or interactive_session_required" },
          "409": { "description": "review_changed; refresh Home before reporting a changed comment" },
          "429": { "description": "report_limit_reached or rate_limited; Retry-After header included" }
        }
      }
    },
    "/monitors/{id}/status-history": {
      "get": {
        "summary": "List recent monitor status periods (newest first)",
        "description": "Returns status periods, newest first. The open period has to_status=null and ended_at=null (the current status, not a completed UP<->DOWN change). Filter to_status != null when counting completed status changes. from_http_code is the HTTP code at period start.",
        "operationId": "listMonitorStatusHistory",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 5 },
            "description": "Max periods to return (default 5, cap 100)."
          }
        ],
        "responses": {
          "200": {
            "description": "Recent status periods",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status_history"],
                  "properties": {
                    "status_history": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/StatusHistoryEntry" }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Monitor not found for this user"
          }
        }
      }
    },
    "/auth/signup": {
      "post": {
        "security": [],
        "summary": "Start signup and email a verification OTP",
        "operationId": "signup",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "password"],
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "password": { "type": "string", "minLength": 8, "maxLength": 256 }
                }
              }
            }
          }
        },
        "responses": {
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "200": {
            "description": "Verification code sent"
          },
          "400": {
            "description": "invalid_input - also returned for reserved RFC 2606 example domains (example.com/.net/.org and subdomains), which are rejected before any pending signup row or email send"
          },
          "503": {
            "description": "Verification email could not be sent; pending row rolled back"
          }
        }
      }
    },
    "/auth/signup/verify": {
      "post": {
        "security": [],
        "summary": "Verify signup OTP and create the account",
        "description": "Requires the same password bound at signup so an attacker cannot plant a password via a victim-entered OTP.",
        "operationId": "signupVerify",
        "parameters": [
          {
            "name": "X-CodaURL-Client",
            "in": "header",
            "required": false,
            "description": "Optional client label. Only the mobile app values are recognized; they label the signup channel in the admin signup alert.",
            "schema": { "type": "string", "enum": ["mobile", "mobile-ios", "mobile-android"] }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "code", "password"],
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "code": { "type": "string", "pattern": "^\\d{6}$" },
                  "password": { "type": "string", "minLength": 8, "maxLength": 256 }
                }
              }
            }
          }
        },
        "responses": {
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "201": {
            "description": "Account created; session tokens issued (BFF cookies them)"
          },
          "401": {
            "description": "Password does not match the signup-bound hash"
          }
        }
      }
    },
    "/auth/login": {
      "post": {
        "security": [],
        "summary": "Log in and issue session tokens",
        "operationId": "login",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EmailPasswordInput" } } }
        },
        "responses": {
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "200": { "description": "Session tokens issued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionTokenResponse" } } } },
          "400": { "description": "invalid_input (malformed email or missing password)" },
          "401": { "description": "invalid_credentials" },
          "429": { "description": "rate_limited or account_locked" }
        }
      }
    },
    "/auth/device/code": {
      "post": {
        "security": [],
        "summary": "Start OAuth Device Authorization",
        "operationId": "createDeviceAuthorization",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeviceCodeInput" } } }
        },
        "responses": {
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "200": { "description": "Device authorization started", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeviceCodeResponse" } } } },
          "400": { "description": "invalid_client" },
          "429": {
            "description": "rate_limited"
          }
        }
      }
    },
    "/auth/device/token": {
      "post": {
        "security": [],
        "summary": "Poll OAuth Device Authorization",
        "operationId": "pollDeviceAuthorization",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeviceTokenInput" } } }
        },
        "responses": {
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "200": { "description": "Session tokens issued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionTokenResponse" } } } },
          "400": { "description": "authorization_pending, access_denied, or expired_token" },
          "429": { "description": "slow_down" }
        }
      }
    },
    "/auth/refresh": {
      "post": {
        "security": [],
        "summary": "Rotate a refresh token",
        "operationId": "refreshSession",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RefreshTokenInput" } } }
        },
        "responses": {
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "200": { "description": "Rotated session tokens", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionTokenResponse" } } } },
          "401": { "description": "invalid_refresh_token" },
          "429": { "description": "rate_limited or account_locked (Retry-After); the refresh token is not consumed" }
        }
      }
    },
    "/auth/restore": {
      "post": {
        "security": [],
        "summary": "Restore a pending-deletion account",
        "operationId": "restoreAccount",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EmailPasswordInput" } } }
        },
        "responses": {
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "200": { "description": "Account restored and session tokens issued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SessionTokenResponse" } } } },
          "401": { "description": "invalid_credentials" },
          "400": { "description": "invalid_input or account_not_deleted" },
          "429": {
            "description": "rate_limited or account_locked"
          }
        }
      }
    },
    "/auth/forgot-password": {
      "post": {
        "security": [],
        "summary": "Request a password reset code",
        "operationId": "requestPasswordReset",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EmailInput" } } }
        },
        "responses": {
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "200": { "description": "Password reset request accepted" },
          "400": { "description": "invalid_input" }
        }
      }
    },
    "/auth/reset-password": {
      "post": {
        "security": [],
        "summary": "Reset a password with a code or token",
        "operationId": "resetPassword",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResetPasswordInput" } } }
        },
        "responses": {
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "200": { "description": "Password reset successful" },
          "400": { "description": "invalid_input or invalid_reset_code" }
        }
      }
    },
    "/auth/logout": {
      "post": {
        "summary": "Revoke the current session",
        "operationId": "logout",
        "responses": { "204": { "description": "Session revoked" } }
      }
    },
    "/me": {
      "delete": {
        "summary": "Start account deletion",
        "operationId": "deleteAccount",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["password"], "properties": { "password": { "type": "string", "description": "Current account password (re-confirmation)" } } } } } },
        "responses": {
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "200": {
            "description": "Account marked for deletion. Web (Stripe) subscriptions are set to end at the period end (a failed attempt is retried before the account is purged); an active App Store or Google Play subscription is not cancelled and is reported in `store_subscription`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "deleted_user_id"],
                  "properties": {
                    "success": { "type": "boolean" },
                    "deleted_user_id": { "type": "string" },
                    "store_subscription": {
                      "type": "object",
                      "description": "Present only when an active Store subscription must be cancelled by the user.",
                      "required": ["management", "message"],
                      "properties": {
                        "management": { "type": "string", "enum": ["app_store", "play_store"] },
                        "message": { "type": "string" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "password_required" },
          "403": { "description": "invalid_password, password_not_set, or interactive_session_required (agent sessions)" },
          "404": { "description": "user_not_found" },
          "429": { "description": "account_locked or rate_limited; failed passwords share the login lockout" }
        }
      }
    },
    "/auth/whoami": {
      "get": {
        "summary": "Get signed-in user profile",
        "operationId": "getWhoami",
        "responses": {
          "200": {
            "description": "User profile",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string" },
                        "email": { "type": "string" },
                        "tier": { "type": "string", "enum": ["free", "pro", "enterprise"] },
                        "max_monitors": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/auth/sessions": {
      "get": {
        "summary": "List signed-in sessions",
        "description": "Returns non-revoked browser and CLI sessions, including expired access-token sessions. Sessions are ordered by latest activity.",
        "operationId": "listAuthSessions",
        "responses": {
          "200": {
            "description": "Active session list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["sessions"],
                  "properties": {
                    "sessions": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/AuthSession" }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/auth/sessions/{id}": {
      "delete": {
        "summary": "Revoke one signed-in session",
        "description": "Revokes an owned session. Missing and foreign session IDs both return session_not_found.",
        "operationId": "revokeAuthSession",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,128}$" }
          }
        ],
        "responses": {
          "204": { "description": "Session revoked" },
          "400": { "description": "invalid_session_id" },
          "404": { "description": "session_not_found" }
        }
      }
    },
    "/me/notifications": {
      "get": {
        "summary": "Get notification channel settings",
        "operationId": "getNotificationSettings",
        "responses": {
          "200": {
            "description": "Notification settings",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/NotificationSettings" }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update notification channel settings",
        "operationId": "updateNotificationSettings",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "webhook_url": { "type": "string", "nullable": true, "description": "Pro and Enterprise only. Free accounts receive 403 pro_required when setting a non-empty URL. A non-empty http(s) URL must have a host that resolves to a public address: an unresolvable or non-public host returns 400 validation_error, and a DNS outage returns 503 dns_unavailable without saving. When a Slack or Discord URL changes, one probe is sent before saving without posting a message (Discord: GET lookup; Slack: empty JSON body); custom endpoints are not probed. HTTP 404 or 410 returns 400 validation_error and nothing is saved, while other statuses and connection failures are accepted." },
                  "email_notifications_enabled": { "type": "boolean" }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated notification settings",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/NotificationSettings" }
              }
            }
          },
          "400": { "description": "validation_error, including a webhook host that does not resolve or is not allowed" },
          "403": { "description": "pro_required when a Free account sets a non-empty webhook URL" },
          "503": { "description": "dns_unavailable when the webhook host could not be verified; nothing is saved" }
        }
      }
    },
    "/me/notifications/webhook/test": {
      "post": {
        "summary": "Send one test message to a webhook",
        "operationId": "testNotificationWebhook",
        "description": "Pro and Enterprise only. Sends a single test message to `webhook_url` from the body, or to the saved webhook when the field is omitted. The URL is not saved. Slack and Discord webhooks receive a plain text message; other endpoints receive JSON with `event: webhook_test`. The delivery result is returned with HTTP 200 even when the endpoint rejects the message. Shares the immediate-check rate limit.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "webhook_url": { "type": "string", "description": "Optional. Defaults to the saved webhook URL." } },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "status_code": { "type": "integer", "nullable": true, "description": "HTTP status returned by the endpoint, or null when it was not reached." },
                    "error": { "type": "string", "nullable": true }
                  }
                }
              }
            }
          },
          "400": { "description": "validation_error: no webhook configured, invalid URL, or a host that does not resolve or is not allowed" },
          "403": { "description": "pro_required for Free accounts" },
          "429": { "description": "rate_limited" },
          "503": { "description": "dns_unavailable when the host could not be verified" }
        }
      }
    },
    "/me/notifications/history": {
      "get": {
        "summary": "List notification history",
        "operationId": "listNotificationHistory",
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } },
          { "name": "cursor", "in": "query", "schema": { "type": "string" } },
          { "name": "monitor_id", "in": "query", "schema": { "type": "string" } },
          { "name": "channel", "in": "query", "description": "Optional channel filter (e.g. fcm, email, telegram, webhook, system). Mobile inbox uses fcm.", "schema": { "type": "string" } },
          { "name": "outcome", "in": "query", "description": "Optional outcome filter (sent, failed, suppressed).", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Notification history",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotificationHistory" } } }
          }
        }
      }
    },
    "/me/notifications/history/{id}": {
      "patch": {
        "summary": "Mark one notification event read or unread",
        "operationId": "setNotificationEventRead",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["is_read"], "properties": { "is_read": { "type": "boolean" } }, "additionalProperties": false } } }
        },
        "responses": {
          "200": {
            "description": "Updated read state",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "is_read": { "type": "boolean" } } } } }
          },
          "404": { "description": "Notification event not found for this user" }
        }
      }
    },
    "/me/notifications/history/read-all": {
      "post": {
        "summary": "Mark every unread notification event as read",
        "operationId": "markAllNotificationEventsRead",
        "parameters": [
          { "name": "channel", "in": "query", "description": "Optional channel scope. When omitted, all channels are marked read. Mobile uses channel=fcm.", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Number of events updated",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "updated": { "type": "integer" } } } } }
          }
        }
      }
    },
    "/me/devices": {
      "get": {
        "summary": "List the caller's registered push devices",
        "operationId": "listDevices",
        "description": "When `install_id` is supplied, the matching device (if any) is marked `is_current` in the response.",
        "parameters": [
          { "name": "install_id", "in": "query", "required": false, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Registered devices",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "devices": { "type": "array", "items": { "$ref": "#/components/schemas/PushDevice" } } } } } }
          }
        }
      },
      "post": {
        "summary": "Register an FCM device",
        "operationId": "registerDevice",
        "description": "Mobile app only. Requires a user session and valid Firebase App Check token. Identity is scoped to (user, `install_id`): a rotated token replaces the same device row. A user's device count is capped at 20. Tokens registered to another account cannot be reassigned.",
        "parameters": [{ "name": "X-Firebase-AppCheck", "in": "header", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeviceRegistration" } } } },
        "responses": { "201": { "description": "Device registered", "content": { "application/json": { "schema": { "type": "object", "properties": { "device": { "$ref": "#/components/schemas/PushDevice" } } } } } }, "403": { "description": "mobile_app_required" }, "409": { "description": "device_token_conflict" } }
      },
      "delete": {
        "summary": "Remove an FCM device by token",
        "operationId": "deleteDevice",
        "description": "Mobile app only. Requires a user session and valid Firebase App Check token.",
        "parameters": [{ "name": "X-Firebase-AppCheck", "in": "header", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["token"], "properties": { "token": { "type": "string" } }, "additionalProperties": false } } } },
        "responses": { "200": { "description": "Device removed" }, "403": { "description": "mobile_app_required" } }
      }
    },
    "/me/devices/{id}": {
      "delete": {
        "summary": "Remotely revoke a registered device by id",
        "operationId": "revokeDevice",
        "description": "Scoped to the caller; ids belonging to another user return 404.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": { "description": "Device removed" },
          "404": { "description": "Device not found or not owned by the caller" }
        }
      }
    },
    "/me/billing": {
      "get": {
        "summary": "Get resolved subscription status",
        "operationId": "getBilling",
        "description": "Single resolved subscription for the account. Providers are never merged; a second active subscription is reported via `duplicate` and `duplicates`. `can_start_checkout` is false while any subscription is active or a manual Plan override applies.",
        "responses": {
          "200": {
            "description": "Billing status",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BillingStatus" }
              }
            }
          }
        }
      }
    },
    "/me/billing/stripe/checkout": {
      "post": {
        "summary": "Start a Stripe Checkout session for Pro",
        "operationId": "startStripeCheckout",
        "description": "Web billing. Blocked with 403 `stripe_checkout_blocked` when web checkout is disabled for the account; 409 `subscription_already_active` while any subscription is active or a manual Plan override applies; 409 `checkout_in_progress` while another Checkout Session is being created or is still within the short per-user lock TTL. After the TTL, the previous Session is verified: an open Session is reused for the same plan, and must be expired before switching plans. Uncertain creation failures retain the idempotency key; verification failures never create another Session; 503 `billing_not_configured` when web billing is disabled.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "plan": { "type": "string", "enum": ["monthly", "annual"], "default": "monthly" } },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout session created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["url", "session_id", "plan"],
                  "properties": {
                    "url": { "type": "string", "format": "uri" },
                    "session_id": { "type": "string" },
                    "plan": { "type": "string", "enum": ["monthly", "annual"] }
                  }
                }
              }
            }
          },
          "403": { "description": "stripe_checkout_blocked when web checkout is disabled for the account" },
          "409": { "description": "subscription_already_active or checkout_in_progress" },
          "413": { "description": "Request body exceeds 4 KiB (request_too_large)" },
          "429": { "description": "rate_limited: too many checkout attempts" },
          "502": { "description": "billing_upstream_error: Stripe could not create or verify the checkout; retry later. A Stripe 4xx rejection is passed through with its own status." },
          "503": { "description": "billing_not_configured" }
        }
      }
    },
    "/me/billing/stripe/portal": {
      "post": {
        "summary": "Open the Stripe Customer Portal",
        "operationId": "openStripePortal",
        "description": "Payment method, cancellation and invoices for a web subscription. 404 `stripe_customer_not_found` when the account never started web billing.",
        "responses": {
          "200": {
            "description": "Portal session created",
            "content": {
              "application/json": {
                "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string", "format": "uri" } } }
              }
            }
          },
          "404": { "description": "stripe_customer_not_found" },
          "429": { "description": "rate_limited: too many portal attempts" },
          "502": { "description": "billing_upstream_error: Stripe could not open the portal; retry later. A Stripe 4xx rejection is passed through with its own status." },
          "503": { "description": "billing_not_configured" }
        }
      }
    },
    "/me/billing/sync": {
      "post": {
        "summary": "Synchronize RevenueCat entitlement",
        "operationId": "syncBilling",
        "description": "Mobile app only. Requires a user session and valid Firebase App Check token. Limited to five requests per user per 60 seconds. An overlapping newer sync supersedes the older snapshot with 503 billing_sync_superseded; retry the sync.",
        "parameters": [{ "name": "X-Firebase-AppCheck", "in": "header", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Updated user profile" }, "403": { "description": "mobile_app_required" }, "429": { "description": "rate_limited; Retry-After header is included" } }
      }
    },
    "/status-pages": {
      "get": {
        "summary": "List the caller's status pages",
        "operationId": "listStatusPages",
        "responses": {
          "200": {
            "description": "Status pages owned by the caller",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status_pages": { "type": "array", "items": { "$ref": "#/components/schemas/StatusPage" } }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a status page",
        "operationId": "createStatusPage",
        "description": "Created unpublished. At most 5 status pages per account.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["slug", "title"],
                "properties": {
                  "slug": { "type": "string", "pattern": "^[a-z0-9-]{3,40}$", "example": "acme-status" },
                  "title": { "type": "string", "maxLength": 120, "example": "Acme Service Status" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Status page created",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "status_page": { "$ref": "#/components/schemas/StatusPage" } } }
              }
            }
          },
          "409": { "description": "slug_taken or status_page_limit_reached" }
        }
      }
    },
    "/status-pages/{id}": {
      "get": {
        "summary": "Get one status page",
        "operationId": "getStatusPage",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Status page detail",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "status_page": { "$ref": "#/components/schemas/StatusPage" } } }
              }
            }
          },
          "404": { "description": "Status page not found for this user" }
        }
      },
      "patch": {
        "summary": "Edit status page title/slug",
        "operationId": "editStatusPage",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "slug": { "type": "string", "pattern": "^[a-z0-9-]{3,40}$" },
                  "title": { "type": "string", "maxLength": 120 }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status page updated",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "status_page": { "$ref": "#/components/schemas/StatusPage" } } }
              }
            }
          },
          "404": { "description": "Status page not found for this user" },
          "409": { "description": "slug_taken" }
        }
      },
      "delete": {
        "summary": "Delete a status page",
        "operationId": "deleteStatusPage",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Status page deleted",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "id": { "type": "string" }, "deleted": { "type": "boolean" } } }
              }
            }
          },
          "404": { "description": "Status page not found for this user" }
        }
      }
    },
    "/status-pages/{id}/publish": {
      "post": {
        "summary": "Publish a status page",
        "operationId": "publishStatusPage",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Status page published",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "status_page": { "$ref": "#/components/schemas/StatusPage" } } }
              }
            }
          },
          "404": { "description": "Status page not found for this user" }
        }
      }
    },
    "/status-pages/{id}/unpublish": {
      "post": {
        "summary": "Unpublish a status page",
        "operationId": "unpublishStatusPage",
        "description": "Public access is blocked immediately in the data center that handled this request and within the cache TTL (max ~30s) elsewhere.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Status page unpublished",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "status_page": { "$ref": "#/components/schemas/StatusPage" } } }
              }
            }
          },
          "404": { "description": "Status page not found for this user" }
        }
      }
    },
    "/status-pages/{id}/monitors": {
      "put": {
        "summary": "Replace the monitors shown on a status page",
        "operationId": "replaceStatusPageMonitors",
        "description": "Full replace. Each monitor_id must belong to the caller (404 monitor_not_found otherwise). At most 20 monitors per page.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["monitors"],
                "properties": {
                  "monitors": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["monitor_id", "display_name"],
                      "properties": {
                        "monitor_id": { "type": "string" },
                        "display_name": { "type": "string", "maxLength": 80 },
                        "sort_order": { "type": "integer", "minimum": 0, "maximum": 10000 }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Monitor list replaced",
            "content": {
              "application/json": {
                "schema": { "type": "object", "properties": { "status_page": { "$ref": "#/components/schemas/StatusPage" } } }
              }
            }
          },
          "404": { "description": "Status page or monitor not found for this user" },
          "409": { "description": "status_page_monitor_limit_reached" }
        }
      }
    },
    "/status/{slug}": {
      "servers": [{ "url": "https://api.codaurl.com", "description": "Public status page - outside /api/v1" }],
      "get": {
        "summary": "Public status page (no auth)",
        "operationId": "getPublicStatusPage",
        "security": [],
        "description": "Returns HTML by default; JSON when Accept: application/json. Only monitors the owner published are included; url/error detail/paused_reason are never exposed.",
        "parameters": [{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Status page (HTML or JSON per Accept header)",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/PublicStatusPageView" } },
              "text/html": { "schema": { "type": "string" } }
            }
          },
          "404": { "description": "status_page_not_found - unpublished, deleted, or unknown slug (same shape, no enumeration signal)" },
          "429": { "description": "rate_limited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "StatusPage": {
        "type": "object",
        "required": ["id", "slug", "title", "published", "created_at", "updated_at", "monitors"],
        "properties": {
          "id": { "type": "string" },
          "slug": { "type": "string" },
          "title": { "type": "string" },
          "published": { "type": "boolean" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "monitors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "monitor_id": { "type": "string" },
                "display_name": { "type": "string" },
                "sort_order": { "type": "integer" }
              }
            }
          }
        }
      },
      "PublicStatusPageView": {
        "type": "object",
        "required": ["title", "overall_status", "generated_at", "monitors"],
        "properties": {
          "title": { "type": "string" },
          "overall_status": { "type": "string", "enum": ["UP", "DOWN", "UNKNOWN", "PAUSED"] },
          "generated_at": { "type": "string", "format": "date-time" },
          "monitors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "display_name": { "type": "string" },
                "status": { "type": "string", "enum": ["UP", "DOWN", "UNKNOWN", "PAUSED"] },
                "stale": { "type": "boolean" },
                "uptime_90d_percent": { "type": "number", "nullable": true, "description": "Uptime percentage. The name is kept for compatibility; the value covers the last 45 days, matching closed state-period retention." },
                "last_checked_at": { "type": "string", "format": "date-time", "nullable": true },
                "incidents": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "started_at": { "type": "string", "format": "date-time" },
                      "ended_at": { "type": "string", "format": "date-time", "nullable": true },
                      "ongoing": { "type": "boolean" }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "BillingStatus": {
        "type": "object",
        "required": ["tier", "active", "source", "plan_source", "status", "current_period_end", "cancel_at_period_end", "can_start_checkout", "management", "duplicate"],
        "properties": {
          "tier": { "type": "string", "enum": ["free", "pro", "enterprise"] },
          "active": { "type": "boolean" },
          "source": { "type": "string", "nullable": true, "enum": ["apple", "google", "stripe"] },
          "plan_source": { "type": "string", "nullable": true, "enum": ["apple", "google", "stripe", "enterprise", "assigned"] },
          "status": { "type": "string", "nullable": true, "enum": ["active", "past_due", "incomplete", "canceled", "unpaid", "paused"] },
          "current_period_end": { "type": "string", "format": "date-time", "nullable": true },
          "cancel_at_period_end": { "type": "boolean" },
          "can_start_checkout": { "type": "boolean" },
          "management": { "type": "string", "enum": ["stripe_portal", "app_store", "play_store", "none"] },
          "duplicate": { "type": "boolean" },
          "duplicates": {
            "type": "array",
            "description": "Present only when duplicate is true.",
            "items": {
              "type": "object",
              "properties": {
                "source": { "type": "string", "enum": ["apple", "google", "stripe"] },
                "management": { "type": "string", "enum": ["stripe_portal", "app_store", "play_store", "none"] }
              }
            }
          }
        }
      },
      "ExpectedStatusCode": {
        "type": "integer",
        "minimum": 100,
        "maximum": 599
      },
      "ExpectedStatus": {
        "type": "array",
        "description": "Allowlist of HTTP status codes that count as UP. Sorted unique. 1-8 items.",
        "minItems": 1,
        "maxItems": 8,
        "items": { "$ref": "#/components/schemas/ExpectedStatusCode" },
        "example": [200, 204, 301]
      },
      "ExpectedStatusInput": {
        "description": "Single code or allowlist. Normalized to ExpectedStatus on write.",
        "oneOf": [
          { "$ref": "#/components/schemas/ExpectedStatusCode" },
          { "$ref": "#/components/schemas/ExpectedStatus" }
        ],
        "default": 200
      },
      "RequestHeadersInput": {
        "type": "array",
        "description": "Pro/Enterprise only. Replaces all stored headers; [] clears them. Up to 5 headers and 2048 UTF-8 bytes across names and values. Host, Content-Length, Connection, Transfer-Encoding, Cookie and other transport or proxy headers are rejected. Values are encrypted at rest and write-only in responses.",
        "maxItems": 5,
        "items": {
          "type": "object",
          "required": ["name", "value"],
          "additionalProperties": false,
          "properties": {
            "name": { "type": "string", "description": "HTTP field name." },
            "value": { "type": "string", "minLength": 1, "writeOnly": true, "description": "Secret header value." }
          }
        }
      },
      "SslInfo": {
        "description": "SSL/TLS certificate expiry information. Free-tier monitors return only the entitlement marker; Pro and Enterprise monitors return the latest daily peer certificate observation and independent TLS validation result.",
        "oneOf": [
          {
            "type": "object",
            "required": ["entitlement"],
            "properties": {
              "entitlement": { "type": "string", "enum": ["pro_required"] }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": ["valid_to", "days_remaining", "issuer", "status", "error_code", "notification_stage", "source", "last_checked_at"],
            "properties": {
              "valid_to": { "type": "string", "format": "date-time" },
              "days_remaining": { "type": "integer" },
              "issuer": { "type": "string", "nullable": true },
              "status": { "type": "string", "enum": ["valid", "warning", "critical", "expired", "error"] },
              "error_code": { "type": "string", "nullable": true, "enum": ["invalid_hostname", "peer_cert_timeout", "peer_cert_unavailable", "peer_cert_invalid_response", "cert_not_found", "ct_log_http_error", "ct_log_timeout", "ct_log_unavailable", "ct_log_invalid_response"], "description": "Reason direct peer certificate observation failed; null after a successful observation. Legacy CT errors may remain in older snapshots. Independent of tls.error_code." },
              "notification_stage": { "type": "string", "enum": ["d30", "d14", "d7", "d3", "d1", "expired"], "nullable": true, "description": "Current owner reminder stage of the certificate: d7, d3 or d1 by the time left to valid_to, expired after valid_to, null otherwise. d30 and d14 are retired and no longer returned." },
              "source": { "type": "string", "enum": ["live_tls", "ct_log"], "description": "live_tls for direct peer observation; ct_log may remain in older snapshots until refreshed." },
              "last_checked_at": { "type": "string", "format": "date-time" },
              "tls": {
                "type": "object",
                "required": ["status", "error_code", "last_checked_at"],
                "properties": {
                  "status": { "type": "string", "enum": ["unknown", "ok", "error"] },
                  "error_code": { "type": "string", "nullable": true },
                  "last_checked_at": { "type": "string", "format": "date-time", "nullable": true }
                },
                "additionalProperties": false
              }
            },
            "additionalProperties": false
          }
        ]
      },
      "DomainInfo": {
        "description": "Registered-domain expiry watch (RDAP, checked daily). Free-tier monitors return only the entitlement marker; Pro and Enterprise monitors return the cached expiry of the monitor host's registered domain (Public Suffix List). Hosts without a registered domain, such as IP literals, omit the object.",
        "oneOf": [
          {
            "type": "object",
            "required": ["entitlement"],
            "properties": {
              "entitlement": { "type": "string", "enum": ["pro_required"] }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": ["name", "state", "expires_at", "days_remaining", "registrar", "error_code", "last_checked_at"],
            "properties": {
              "name": { "type": "string", "description": "Registered domain the expiry belongs to, for example example.co.uk." },
              "state": { "type": "string", "enum": ["ok", "expiring", "expired", "unsupported", "unknown"], "description": "ok: more than 30 days left; expiring: 30 days or less; expired: past expires_at; unsupported: the TLD publishes no RDAP service; unknown: no expiry known yet (first daily check pending, or lookups keep failing)." },
              "expires_at": { "type": "string", "format": "date-time", "nullable": true },
              "days_remaining": { "type": "integer", "nullable": true, "description": "Whole days rounded up; zero or negative once expired." },
              "registrar": { "type": "string", "nullable": true },
              "error_code": { "type": "string", "nullable": true, "enum": ["unsupported_tld", "rdap_not_found", "rdap_no_expiration", "rdap_http_error", "rdap_timeout", "rdap_invalid", "bootstrap_unavailable"], "description": "Reason the latest lookup failed; the previous expires_at is kept. Null after a successful lookup." },
              "last_checked_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Time of the latest successful lookup." }
            },
            "additionalProperties": false
          }
        ]
      },
      "NotificationSettings": {
        "type": "object",
        "properties": {
          "webhook_url": { "type": "string", "nullable": true },
          "email": { "type": "string", "nullable": true, "readOnly": true },
          "email_notifications_enabled": { "type": "boolean" }
        }
      },
      "NotificationHistory": {
        "type": "object",
        "required": ["events", "next_cursor"],
        "properties": {
          "events": { "type": "array", "items": { "$ref": "#/components/schemas/NotificationEvent" } },
          "next_cursor": { "type": "string", "nullable": true }
        }
      },
      "NotificationEvent": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" }, "monitor_id": { "type": "string", "nullable": true, "description": "Public id of the monitor the event was recorded for. Kept after the monitor is deleted; null for account-level events." },
          "kind": { "type": "string" }, "channel": { "type": "string" }, "outcome": { "type": "string" },
          "reason": { "type": "string", "nullable": true }, "status": { "type": "string", "nullable": true },
          "previous_status": { "type": "string", "nullable": true }, "http_code": { "type": "integer", "nullable": true },
          "title": { "type": "string", "nullable": true }, "body": { "type": "string", "nullable": true },
          "created_at": { "type": "string", "format": "date-time" },
          "is_read": { "type": "boolean", "description": "Not an Agent judgment field; read state is client UI-only." }
        }
      },
      "DeviceRegistration": {
        "type": "object",
        "required": ["token", "platform", "install_id"],
        "properties": {
          "token": { "type": "string" },
          "platform": { "type": "string", "enum": ["android", "ios"] },
          "install_id": { "type": "string", "maxLength": 128, "description": "Stable per-install identifier generated on-device. Not a hardware id. Scopes token rotation to the same device row." },
          "device_name": { "type": "string", "maxLength": 120 },
          "app_version": { "type": "string", "maxLength": 40 }
        },
        "additionalProperties": false
      },
      "PushDevice": {
        "type": "object",
        "required": ["id", "platform", "token_masked", "is_current", "created_at", "updated_at", "last_seen_at"],
        "properties": {
          "id": { "type": "integer" },
          "platform": { "type": "string", "enum": ["android", "ios"] },
          "device_name": { "type": "string", "nullable": true },
          "app_version": { "type": "string", "nullable": true },
          "token_masked": { "type": "string" },
          "is_current": { "type": "boolean", "description": "true when GET was called with the caller's own `install_id`." },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "last_seen_at": { "type": "string", "format": "date-time" }
        }
      },
      "EmailPasswordInput": {
        "type": "object",
        "required": ["email", "password"],
        "properties": {
          "email": { "type": "string", "format": "email" },
          "password": { "type": "string", "minLength": 8, "maxLength": 256 }
        },
        "additionalProperties": false
      },
      "EmailInput": {
        "type": "object",
        "required": ["email"],
        "properties": { "email": { "type": "string", "format": "email" } },
        "additionalProperties": false
      },
      "RefreshTokenInput": {
        "type": "object",
        "required": ["refresh_token"],
        "properties": { "refresh_token": { "type": "string" } },
        "additionalProperties": false
      },
      "DeviceCodeInput": {
        "type": "object",
        "required": ["client_id"],
        "properties": {
          "client_id": { "type": "string", "enum": ["codaurl-cli"] },
          "scope": { "type": "string", "enum": ["monitor:read monitor:write"] }
        },
        "additionalProperties": false
      },
      "DeviceTokenInput": {
        "type": "object",
        "required": ["client_id", "device_code"],
        "properties": {
          "client_id": { "type": "string", "enum": ["codaurl-cli"] },
          "device_code": { "type": "string" }
        },
        "additionalProperties": false
      },
      "DeviceCodeResponse": {
        "type": "object",
        "required": ["device_code", "user_code", "verification_uri", "expires_in", "interval"],
        "properties": {
          "device_code": { "type": "string" },
          "user_code": { "type": "string", "pattern": "^[A-Z0-9]{4}-[A-Z0-9]{4}$" },
          "verification_uri": { "type": "string", "format": "uri", "description": "Complete Console Device Flow URL including `device=1`. Clients append the URL-encoded `user_code` query parameter." },
          "expires_in": { "type": "integer", "enum": [600] },
          "interval": { "type": "integer", "minimum": 5 }
        }
      },
      "ResetPasswordInput": {
        "type": "object",
        "required": ["email", "new_password"],
        "properties": {
          "email": { "type": "string", "format": "email" },
          "code": { "type": "string", "description": "One-time code, or provide token instead." },
          "token": { "type": "string", "description": "Password reset token, or provide code instead." },
          "new_password": { "type": "string", "minLength": 8, "maxLength": 256 }
        },
        "additionalProperties": false
      },
      "SessionTokenResponse": {
        "type": "object",
        "required": ["success", "token", "token_type", "expires_at", "user"],
        "properties": {
          "success": { "type": "boolean", "enum": [true] },
          "token": { "type": "string" },
          "token_type": { "type": "string", "enum": ["Bearer"] },
          "expires_at": { "type": "string", "format": "date-time" },
          "refresh_token": { "type": "string" },
          "refresh_expires_at": { "type": "string", "format": "date-time" },
          "user": { "type": "object" }
        }
      },
      "AuthSession": {
        "type": "object",
        "required": ["id", "created_at", "expires_at", "is_current"],
        "properties": {
          "id": { "type": "string", "example": "sess_0123456789abcdef0123456789abcdef" },
          "user_agent": { "type": "string", "nullable": true, "maxLength": 512 },
          "ip": { "type": "string", "nullable": true, "maxLength": 512 },
          "created_at": { "type": "string", "format": "date-time" },
          "last_seen_at": { "type": "string", "format": "date-time", "nullable": true },
          "expires_at": { "type": "string", "format": "date-time" },
          "is_current": { "type": "boolean" }
        }
      },
      "StatusHistoryEntry": {
        "type": "object",
        "description": "One status period. Newest-first. The open current period has to_status=null and ended_at=null.",
        "properties": {
          "from_status": { "type": "string", "enum": ["UP", "DOWN", "UNKNOWN", "PAUSED"] },
          "to_status": { "type": "string", "enum": ["UP", "DOWN", "UNKNOWN", "PAUSED"], "nullable": true },
          "from_http_code": { "type": "integer", "nullable": true },
          "to_http_code": { "type": "integer", "nullable": true },
          "started_at": { "type": "string", "format": "date-time" },
          "ended_at": { "type": "string", "format": "date-time", "nullable": true },
          "source": { "type": "string", "nullable": true },
          "error_code": { "type": "string", "nullable": true }
        }
      },
      "AutomaticBaselinePreviewPoint": {
        "type": "object",
        "required": ["checked_at", "latency_ms", "median_ms", "upper_bound_ms", "performance_status", "event_kind"],
        "properties": {
          "checked_at": { "type": "string", "format": "date-time" },
          "latency_ms": { "type": "integer", "minimum": 0 },
          "median_ms": { "type": "integer", "nullable": true },
          "upper_bound_ms": { "type": "integer", "nullable": true },
          "performance_status": { "type": "string", "enum": ["UNKNOWN", "NORMAL", "SLOW"] },
          "event_kind": { "type": "string", "nullable": true, "enum": ["latency_warning", "latency_recovery"] }
        }
      },
      "Monitor": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "nullable": true, "maxLength": 80, "description": "Optional user-defined monitor name." },
          "request_headers": { "type": "array", "description": "Header names only. Values are never returned.", "items": { "type": "object", "required": ["name", "has_value"], "properties": { "name": { "type": "string" }, "has_value": { "type": "boolean", "enum": [true] } } } },
          "keyword": { "type": "string", "nullable": true, "description": "Case-sensitive response body keyword. Null disables body checks." },
          "keyword_mode": { "type": "string", "nullable": true, "enum": ["contains", "not_contains"], "description": "Keyword condition; null when no keyword is configured." },
          "keyword_matched": { "type": "boolean", "nullable": true, "description": "Latest keyword condition verdict. Null before a keyword check, after changing the condition, or when HTTP/body could not be evaluated." },
          "check_options_entitlement": { "type": "string", "enum": ["included", "pro_required"], "description": "Whether paid check options apply. pro_required (Free): stored request_headers, keyword and keyword_mode are kept but not applied to checks; keyword_matched stays null. included on Pro and Enterprise." },
          "latency_baseline_enabled": { "type": "boolean", "description": "Whether the paid automatic response baseline is enabled." },
          "latency_baseline": {
            "type": "object",
            "nullable": true,
            "description": "Automatic baseline state. null when automatic baseline mode is disabled.",
            "required": ["status", "median_ms", "upper_bound_ms", "sample_count", "computed_at"],
            "properties": {
              "status": { "type": "string", "enum": ["LEARNING", "READY"] },
              "median_ms": { "type": "integer", "nullable": true, "minimum": 0 },
              "upper_bound_ms": { "type": "integer", "nullable": true, "minimum": 0 },
              "sample_count": { "type": "integer", "minimum": 0 },
              "computed_at": { "type": "string", "format": "date-time", "nullable": true }
            }
          },
          "performance_status": { "type": "string", "enum": ["DISABLED", "UNKNOWN", "NORMAL", "SLOW"], "description": "Independent of availability UP/DOWN; scheduled checks only." },
          "performance_since": { "type": "string", "format": "date-time", "nullable": true },
          "id": { "type": "string" },
          "url": { "type": "string" },
          "check_interval_seconds": {
            "type": "integer",
            "enum": [60, 300, 900, 1800, 3600]
          },
          "expected_status": { "$ref": "#/components/schemas/ExpectedStatus" },
          "is_active": { "type": "boolean" },
          "paused_reason": {
            "type": "string",
            "nullable": true,
            "description": "Why is_active is false. manual (user or agent), admin (operator), auto_flapping (automatic pause after repeated UP/DOWN flapping), inactive_14d (Free account inactive for 14 days), plan_downgrade (Free plan limit exceeded; rename or delete only), user_deleted (account deletion in progress), or null. Not a closed enum: treat unknown values as paused."
          },
          "muted_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "ISO mute end while active; null when unset or expired. Mute is not pause."
          },
          "status": { "type": "string", "nullable": true },
          "last_previous_status": { "type": "string", "nullable": true },
          "status_since": { "type": "string", "nullable": true },
          "last_status_changed": { "type": "boolean", "nullable": true },
          "last_http_code": { "type": "integer", "nullable": true },
          "last_latency_ms": { "type": "integer", "nullable": true },
          "last_error_code": { "type": "string", "nullable": true, "enum": ["unexpected_status", "keyword_mismatch", "timeout", "network_error", "ssrf_blocked", "checker_error"] },
          "last_error": { "type": "string", "nullable": true },
          "last_up_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "ISO time of the most recent UP check; null before the first check."
          },
          "ssl": {
            "$ref": "#/components/schemas/SslInfo",
            "description": "Free: pro_required marker. Pro/Enterprise: daily live peer certificate observation plus an independent validated TLS handshake result. Omitted when no Pro/Enterprise snapshot exists yet."
          },
          "domain": {
            "$ref": "#/components/schemas/DomainInfo",
            "description": "Registered-domain expiry (RDAP, daily). Returned by GET /monitors and GET /monitors/{id}; free accounts get the pro_required marker. Omitted for hosts without a registered domain."
          }
        }
      }
    }
  }
}
