{
  "openapi": "3.1.0",
  "info": {
    "title": "Is the Power On — Device API",
    "version": "1.0.0",
    "description": "Public heartbeat, status, and headless ESP32 registration endpoints. No browser login is needed for these calls. The path token or per-device secret is the credential. Keep credentials private and use HTTPS."
  },
  "servers": [
    { "url": "/api", "description": "Same origin as the website, over HTTPS" }
  ],
  "externalDocs": {
    "description": "Human-readable documentation and examples",
    "url": "/api-docs"
  },
  "tags": [
    { "name": "Heartbeat", "description": "Write and read a power monitor's status." },
    { "name": "Hardware", "description": "Register and poll a headless transmitter or watcher before or after pairing." }
  ],
  "x-agent-notes": [
    "Substitute the website's HTTPS origin for YOUR_SITE_ORIGIN. These paths are relative to /api.",
    "Never expose a real deviceToken or hardware secret in prompts, code, telemetry, or public logs.",
    "Register does not create or claim a monitor. A signed-in user on the same public IP claims the pending hardware in the dashboard.",
    "The paired transmitter token can POST ping and GET status. The paired watcher token begins w_ and can only GET status."
  ],
  "paths": {
    "/device-heartbeats/{deviceToken}/ping": {
      "post": {
        "operationId": "recordDevicePing",
        "tags": ["Heartbeat"],
        "summary": "Record a power heartbeat",
        "description": "Records the server's current time as a ping, updates the monitor, and may trigger configured alerts. Send periodically while power is present, more frequently than the monitor's offline threshold. No request body or logged-in session is required. A watcher token is not authorized for this write endpoint.",
        "parameters": [{ "$ref": "#/components/parameters/DeviceToken" }],
        "responses": {
          "200": {
            "description": "Ping recorded",
            "content": { "application/json": {
              "schema": { "$ref": "#/components/schemas/PingSuccess" },
              "example": { "success": true, "pingedAt": "2026-09-28T12:00:00.000Z" }
            } }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "404": { "$ref": "#/components/responses/UnknownToken" }
        }
      }
    },
    "/device-heartbeats/{deviceToken}/status": {
      "get": {
        "operationId": "getDeviceStatus",
        "tags": ["Heartbeat"],
        "summary": "Read current power status",
        "description": "Returns on if the most recent ping is within the monitor's configured threshold; otherwise returns off. Before any ping, lastPingAt is null. Both a monitor token and a paired watcher's read-only w_ token work here. Reading status does not create a heartbeat.",
        "parameters": [{ "$ref": "#/components/parameters/DeviceToken" }],
        "responses": {
          "200": {
            "description": "Current status",
            "content": { "application/json": {
              "schema": { "$ref": "#/components/schemas/DeviceStatus" },
              "examples": {
                "on": { "value": { "status": "on", "lastPingAt": "2026-09-28T12:00:00.000Z" } },
                "neverSeen": { "value": { "status": "off", "lastPingAt": null } }
              }
            } }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "404": { "$ref": "#/components/responses/UnknownToken" }
        }
      }
    },
    "/hardware/register": {
      "post": {
        "operationId": "registerHardware",
        "tags": ["Hardware"],
        "summary": "Register a headless device or poll for its token",
        "description": "Send the stable ID, role and unique per-device secret on boot and every 15–30 seconds while pending. The first successful registration binds the ID to the secret (trust on first use), without factory ownership proof. A signed-in user on the same recently observed public IP must claim the device through the dashboard, attaching a transmitter to an existing or new monitor, or a watcher to an already transmitter-backed monitor. Repeat this same request after claim to receive or recover the operational token. Pending discovery lasts three minutes after the last poll; continuously polled unclaimed IDs reset after one hour. Registration needs trusted HTTPS ingress to determine the public IP.",
        "requestBody": {
          "required": true,
          "content": { "application/json": {
            "schema": { "$ref": "#/components/schemas/HardwareRegistrationInput" },
            "example": {
              "hardwareId": "esp32_A1B2C3",
              "type": "transmitter",
              "secret": "PLACEHOLDER_RANDOM_32_PLUS_CHARACTER_SECRET"
            }
          } }
        },
        "responses": {
          "200": {
            "description": "Pending pairing, or the paired device's operational token",
            "content": { "application/json": {
              "schema": { "$ref": "#/components/schemas/HardwareRegistration" },
              "examples": {
                "pending": { "value": { "state": "pending", "deviceToken": null } },
                "paired": { "value": { "state": "paired", "deviceToken": "OPERATIONAL_TOKEN" } }
              }
            } }
          },
          "400": { "$ref": "#/components/responses/InvalidRequest" },
          "409": {
            "description": "The ID is bound to a different secret or type, or the paired monitor no longer exists",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "At most 20 active pending IDs are allowed per observed IP; retry later",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "503": {
            "description": "The trusted network address is unavailable; use the website's HTTPS ingress",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "DeviceToken": {
        "name": "deviceToken",
        "in": "path",
        "required": true,
        "description": "Secret token copied from an owned monitor, or returned to a paired transmitter or watcher by registration. Watcher tokens (w_ prefix) are status-only. Do not disclose or log this URL.",
        "schema": { "type": "string", "minLength": 20 }
      }
    },
    "responses": {
      "InvalidRequest": {
        "description": "Invalid path parameter or registration body",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "UnknownToken": {
        "description": "Unknown token, or a watcher token used for ping",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": { "error": { "type": "string" } }
      },
      "PingSuccess": {
        "type": "object",
        "required": ["success", "pingedAt"],
        "properties": {
          "success": { "type": "boolean", "const": true },
          "pingedAt": { "type": "string", "format": "date-time", "description": "UTC server timestamp." }
        }
      },
      "DeviceStatus": {
        "type": "object",
        "required": ["status", "lastPingAt"],
        "properties": {
          "status": { "type": "string", "enum": ["on", "off"] },
          "lastPingAt": { "type": ["string", "null"], "format": "date-time", "description": "UTC time of most recent ping, or null if never pinged." }
        }
      },
      "HardwareRegistrationInput": {
        "type": "object",
        "required": ["hardwareId", "type", "secret"],
        "properties": {
          "hardwareId": { "type": "string", "minLength": 6, "maxLength": 64, "pattern": "^[A-Za-z0-9_-]+$", "description": "Stable, unique device ID. Prefer unpredictable IDs." },
          "type": { "type": "string", "enum": ["transmitter", "watcher"] },
          "secret": { "type": "string", "minLength": 32, "maxLength": 128, "description": "Random, persistent, unique per-device secret. Never use a MAC address or shared firmware constant." }
        }
      },
      "HardwareRegistration": {
        "oneOf": [
          {
            "type": "object",
            "required": ["state", "deviceToken"],
            "properties": {
              "state": { "type": "string", "const": "pending" },
              "deviceToken": { "type": "null" }
            }
          },
          {
            "type": "object",
            "required": ["state", "deviceToken"],
            "properties": {
              "state": { "type": "string", "const": "paired" },
              "deviceToken": { "type": "string", "minLength": 20, "description": "Transmitter token authorizes ping and status; watcher w_ token authorizes status only." }
            }
          }
        ]
      }
    }
  }
}