{
  "openapi": "3.1.0",
  "info": {
    "title": "PulseAvesta API",
    "version": "1.0.0",
    "description": "Read and manage your PulseAvesta monitors (websites, TCP ports, DNS records): their status, check history and incidents. Authenticate with an API key from Settings → API keys, sent as X-API-Key or Authorization: Bearer. A read-only key gets 403 on POST and DELETE. AI assistants can use the MCP server at https://app.pulse.avesta.co.th/mcp instead.",
    "contact": {"url": "https://app.pulse.avesta.co.th/docs/api"}
  },
  "servers": [{"url": "https://app.pulse.avesta.co.th/api"}],
  "security": [{"apiKey": []}, {"bearer": []}],
  "paths": {
    "/sites": {
      "get": {
        "operationId": "listSites",
        "summary": "List all monitors with their latest check",
        "responses": {
          "200": {"description": "Every monitor on the account.", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/SiteWithStatus"}}}}},
          "401": {"$ref": "#/components/responses/Unauthorized"}
        }
      },
      "post": {
        "operationId": "createSite",
        "summary": "Add a monitor",
        "description": "Only name and url are required; everything else defaults the way the web form does. The interval is clamped into the plan's allowed range.",
        "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CreateSiteRequest"}}}},
        "responses": {
          "201": {"description": "Created.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Site"}}}},
          "400": {"$ref": "#/components/responses/BadRequest"},
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "403": {"$ref": "#/components/responses/Forbidden"},
          "409": {"description": "A monitor for this target already exists.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
        }
      }
    },
    "/sites/{id}": {
      "parameters": [{"$ref": "#/components/parameters/SiteID"}],
      "get": {
        "operationId": "getSite",
        "summary": "Fetch one monitor",
        "responses": {
          "200": {"description": "The monitor.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Site"}}}},
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "404": {"$ref": "#/components/responses/NotFound"}
        }
      },
      "delete": {
        "operationId": "deleteSite",
        "summary": "Delete a monitor and its history",
        "responses": {
          "204": {"description": "Deleted."},
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "403": {"$ref": "#/components/responses/Forbidden"},
          "404": {"$ref": "#/components/responses/NotFound"}
        }
      }
    },
    "/sites/{id}/checks": {
      "parameters": [{"$ref": "#/components/parameters/SiteID"}],
      "get": {
        "operationId": "listChecks",
        "summary": "The monitor's 200 most recent checks, newest first",
        "responses": {
          "200": {"description": "Checks.", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/Check"}}}}},
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "404": {"$ref": "#/components/responses/NotFound"}
        }
      }
    },
    "/sites/{id}/incidents": {
      "parameters": [{"$ref": "#/components/parameters/SiteID"}],
      "get": {
        "operationId": "listIncidents",
        "summary": "The monitor's incidents (outages), newest first, up to 200",
        "responses": {
          "200": {"description": "Incidents.", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/Incident"}}}}},
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "404": {"$ref": "#/components/responses/NotFound"}
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {"type": "apiKey", "in": "header", "name": "X-API-Key"},
      "bearer": {"type": "http", "scheme": "bearer", "description": "An API key (pw_...) or an OAuth access token."}
    },
    "parameters": {
      "SiteID": {"name": "id", "in": "path", "required": true, "schema": {"type": "integer", "format": "int64"}}
    },
    "responses": {
      "BadRequest": {"description": "Invalid request.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "Unauthorized": {"description": "Missing, invalid, revoked or expired key.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "Forbidden": {"description": "A read-only key, or the account's monitor limit is reached.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "NotFound": {"description": "No such monitor on this account.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
    },
    "schemas": {
      "Error": {"type": "object", "required": ["error"], "properties": {"error": {"type": "string"}}},
      "CreateSiteRequest": {
        "type": "object",
        "required": ["name", "url"],
        "properties": {
          "name": {"type": "string"},
          "url": {"type": "string", "description": "https://… for http; a host name for tcp and dns."},
          "check_type": {"type": "string", "enum": ["http", "tcp", "dns"], "default": "http"},
          "check_interval_seconds": {"type": "integer", "description": "0 or absent = the default; clamped into the allowed range."},
          "port": {"type": "integer", "description": "tcp only."},
          "dns_expected_ip": {"type": "string", "description": "dns only."},
          "expected_status_code": {"type": "integer", "description": "http only; absent = any 2xx."},
          "content_selector": {"type": "string", "description": "CSS selector whose text is watched for changes; absent = the whole page."},
          "keyword_trigger": {"type": "string", "description": "Alert when this word appears or disappears."},
          "must_contain": {"type": "array", "items": {"type": "string"}, "maxItems": 10, "description": "http only: fail unless the page shows every phrase."},
          "must_not_contain": {"type": "array", "items": {"type": "string"}, "maxItems": 10, "description": "http only: fail if the page shows any phrase."},
          "domain_check_enabled": {"type": "boolean", "default": true, "description": "Domain expiry reminders."},
          "group_name": {"type": "string", "default": "Default", "description": "Usually the client's name."}
        }
      },
      "Site": {
        "type": "object",
        "properties": {
          "ID": {"type": "integer"},
          "UserID": {"type": "integer"},
          "Name": {"type": "string"},
          "URL": {"type": "string"},
          "CheckIntervalSeconds": {"type": "integer"},
          "CreatedAt": {"type": "string", "format": "date-time"},
          "CheckType": {"type": "string", "enum": ["http", "tcp", "dns"]},
          "Port": {"type": ["integer", "null"]},
          "DNSExpectedIP": {"type": ["string", "null"]},
          "ExpectedStatusCode": {"type": ["integer", "null"]},
          "ContentSelector": {"type": ["string", "null"]},
          "KeywordTrigger": {"type": ["string", "null"]},
          "MustContain": {"type": ["array", "null"], "items": {"type": "string"}},
          "MustNotContain": {"type": ["array", "null"], "items": {"type": "string"}},
          "SSLCheckEnabled": {"type": "boolean"},
          "SSLExpiryAlertDays": {"type": "integer"},
          "SSLExpiresAt": {"type": ["string", "null"], "format": "date-time"},
          "SSLAlertSent": {"type": "boolean"},
          "SSLAlertStage": {"type": ["integer", "null"]},
          "SSLAlertStageExpiresAt": {"type": ["string", "null"], "format": "date-time"},
          "DomainCheckEnabled": {"type": "boolean"},
          "DomainExpiresAt": {"type": ["string", "null"], "format": "date-time"},
          "RetryBeforeAlert": {"type": "integer", "description": "Failed checks in a row before an alert goes out."},
          "ConsecutiveFailures": {"type": "integer"},
          "MaintenanceMode": {"type": "boolean"},
          "GroupName": {"type": "string"},
          "VisibleOnStatusPage": {"type": "boolean"}
        }
      },
      "SiteWithStatus": {
        "allOf": [
          {"$ref": "#/components/schemas/Site"},
          {
            "type": "object",
            "properties": {
              "LastCheckedAt": {"type": ["string", "null"], "format": "date-time"},
              "LastSuccess": {"type": ["boolean", "null"]},
              "LastStatusCode": {"type": ["integer", "null"]},
              "LastLatencyMs": {"type": ["integer", "null"]},
              "LastChangedAt": {"type": ["string", "null"], "format": "date-time", "description": "The last check that saw the content change."},
              "Uptime30d": {"type": ["number", "null"], "description": "Not filled by this endpoint."}
            }
          }
        ]
      },
      "Check": {
        "type": "object",
        "properties": {
          "ID": {"type": "integer"},
          "SiteID": {"type": "integer"},
          "CheckedAt": {"type": "string", "format": "date-time"},
          "Success": {"type": "boolean"},
          "StatusCode": {"type": ["integer", "null"]},
          "ResponseTimeMs": {"type": "integer"},
          "ErrorMessage": {"type": ["string", "null"]},
          "ContentHash": {"type": ["string", "null"]},
          "ContentChanged": {"type": "boolean"},
          "ContentSnapshot": {"type": ["string", "null"], "description": "The page text as checked, kept on content changes."},
          "KeywordMatched": {"type": "boolean"},
          "Timing": {
            "type": "object",
            "description": "Where the response time went, in milliseconds. A phase is null when it didn't happen (no DNS for an IP address, no TLS for plain HTTP or TCP, nothing after the phase a failed check stopped in) and for checks recorded before this was measured.",
            "properties": {
              "DNSMs": {"type": ["integer", "null"], "description": "Resolving the host name."},
              "ConnectMs": {"type": ["integer", "null"], "description": "Opening the TCP connection."},
              "TLSMs": {"type": ["integer", "null"], "description": "The TLS handshake."},
              "TTFBMs": {"type": ["integer", "null"], "description": "From the request sent to the response's first byte: the server's own time."}
            }
          },
          "Detail": {
            "type": ["object", "null"],
            "description": "What the check saw of the connection; null for DNS checks and checks recorded before this was kept.",
            "properties": {
              "ip": {"type": "string", "description": "The address the last connection went to, or the last one tried when none succeeded."},
              "proto": {"type": "string", "description": "HTTP/1.1, HTTP/2.0"},
              "redirects": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": "string"}, "status": {"type": "integer"}}}, "description": "Each hop that answered with a redirect, in order."},
              "final_url": {"type": "string", "description": "Where the redirects ended; absent without redirects."},
              "headers": {"type": "object", "additionalProperties": {"type": "string"}, "description": "Telling response headers (server, CDN, cache), lower-cased names; never Date or request IDs."}
            }
          }
        }
      },
      "Incident": {
        "type": "object",
        "properties": {
          "ID": {"type": "integer"},
          "SiteID": {"type": "integer"},
          "SiteName": {"type": "string"},
          "StartedAt": {"type": "string", "format": "date-time"},
          "ResolvedAt": {"type": ["string", "null"], "format": "date-time", "description": "null while ongoing."},
          "Cause": {"type": "string"},
          "FirstCheckID": {"type": ["integer", "null"]},
          "LastCheckID": {"type": ["integer", "null"]}
        }
      }
    }
  }
}
