{
  "openapi": "3.1.0",
  "info": {
    "title": "pingtxt API",
    "version": "dev",
    "summary": "REST API of the pingtxt hub.",
    "description": "Everything the web UI does goes through this API.\n\n## Authentication\n\nCreate a personal token in **settings → api tokens** and send it as a bearer token:\n\n```\ncurl -H \"Authorization: Bearer ptk_...\" https://status.example.com/api/monitors\n```\n\n- **read** tokens can call every `GET` endpoint marked *read*.\n- **write** tokens can also create, change and delete. Only admins can create them.\n- Account and instance settings (users, tokens, 2FA, settings, backups) need a browser session: tokens get `403`.\n\n## Native apps (OAuth 2.1)\n\nApps sign the user in with OAuth 2.1, authorization code + PKCE (S256), through the hub's own login page (password, 2FA, single sign-on), and get a bearer access token (`pma_...`) that works on the whole API like a token above.\n\n- The iOS app is the built-in public client `pingtxt-ios` with redirect URI `pingtxt://oauth` (no secret, no registration). Other clients register with `POST /oauth/register`.\n- Scopes: `read` or `write`. `write` is granted only to admins; viewers always get `read`. The scope the user picked is in the token response.\n- Access tokens last 1 hour. Refresh tokens last 30 days from their last use and rotate at every refresh: keep only the newest one. Presenting an already used refresh token revokes the sign-in.\n- Sign out with `POST /oauth/revoke`. Users see and disconnect apps in **settings → api tokens → connected apps**.\n- Metadata: `/.well-known/oauth-authorization-server` (RFC 8414) and `/.well-known/oauth-protected-resource/api` (RFC 9728).\n\n## Roles\n\nAdmins can change everything. **Viewers** are read-only: every request that is not `GET` answers `403`, and secrets in monitors and notification channels are replaced by `********`.\n\n## Conventions\n\n- Bodies are JSON (`Content-Type: application/json`); other content types on writes get `415`.\n- Times are Unix milliseconds. Ids are integers.\n- Errors are `{\"error\": \"message\"}` with a 4xx/5xx status.\n- Browser requests from another origin are refused (`403`).\n\nAI assistants can use the same API through the MCP server on `/mcp`.",
    "license": {
      "name": "AGPL-3.0-only",
      "identifier": "AGPL-3.0-only"
    }
  },
  "externalDocs": {
    "description": "pingtxt docs",
    "url": "https://pingtxt.com/docs/"
  },
  "servers": [
    {
      "url": "/",
      "description": "This hub"
    }
  ],
  "tags": [
    {
      "name": "Monitors",
      "description": "Checks, their history and heartbeat (push) URLs."
    },
    {
      "name": "Incidents",
      "description": "Down periods of monitors."
    },
    {
      "name": "Maintenance",
      "description": "Windows during which monitors don't alert."
    },
    {
      "name": "Groups and tags",
      "description": "Organize monitors; status pages and maintenance windows can select them by group or tag."
    },
    {
      "name": "Agents",
      "description": "Remote check locations connected to the hub."
    },
    {
      "name": "Notifications",
      "description": "Alert channels: webhook, Discord, Slack, Telegram, ntfy, email."
    },
    {
      "name": "Reports",
      "description": "Periodic email reports: uptime and incidents of the last week or month, per monitor group."
    },
    {
      "name": "Status pages",
      "description": "Public pages and their data."
    },
    {
      "name": "System",
      "description": "Health, counters, metrics and this document."
    },
    {
      "name": "Auth",
      "description": "Setup, login, password, single sign-on and two-factor. Browser oriented."
    },
    {
      "name": "API tokens",
      "description": "Personal tokens and connected OAuth apps. Browser session only."
    },
    {
      "name": "Users",
      "description": "Accounts and roles. Admin browser session only."
    },
    {
      "name": "Settings",
      "description": "Instance settings. Admin browser session only."
    },
    {
      "name": "Backups",
      "description": "Database backups and configuration export/import."
    },
    {
      "name": "OAuth",
      "description": "OAuth 2.1 authorization server for native apps and MCP clients. Form bodies, OAuth error format."
    }
  ],
  "paths": {
    "/api/agents": {
      "get": {
        "tags": [
          "Agents"
        ],
        "summary": "List agents",
        "operationId": "listAgents",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Agents.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Agent"
                      }
                    },
                    "hub_url": {
                      "type": "string"
                    },
                    "hub_version": {
                      "type": "string"
                    },
                    "auto_update": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Agents"
        ],
        "summary": "Create an agent",
        "operationId": "createAgent",
        "description": "The agent token is returned once.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "location": {
                    "type": "string"
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "fra-1",
                "location": "Frankfurt"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agent": {
                      "$ref": "#/components/schemas/Agent"
                    },
                    "token": {
                      "type": "string",
                      "description": "Agent token (pt_...)."
                    },
                    "hub_url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/agents/{id}": {
      "put": {
        "tags": [
          "Agents"
        ],
        "summary": "Rename an agent",
        "operationId": "updateAgent",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the agent."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "location": {
                    "type": "string"
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "delete": {
        "tags": [
          "Agents"
        ],
        "summary": "Delete an agent",
        "operationId": "deleteAgent",
        "description": "Disconnects it and removes it from monitor locations.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the agent."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/agents/{id}/token": {
      "post": {
        "tags": [
          "Agents"
        ],
        "summary": "Rotate the agent token",
        "operationId": "rotateAgentToken",
        "description": "The agent is disconnected until it uses the new token.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the agent."
          }
        ],
        "responses": {
          "200": {
            "description": "New token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string"
                    },
                    "hub_url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/agents/{id}/update": {
      "post": {
        "tags": [
          "Agents"
        ],
        "summary": "Update an agent now",
        "operationId": "updateAgentNow",
        "description": "Asks a connected agent to install the hub's version.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the agent."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/agents/{id}/monitors": {
      "get": {
        "tags": [
          "Agents"
        ],
        "summary": "Preview compatible monitors",
        "operationId": "listAgentMonitors",
        "description": "Monitors this agent can be added to, and the ones skipped with the reason. Heartbeat monitors have no locations and are left out.\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the agent."
          }
        ],
        "responses": {
          "200": {
            "description": "Compatible and skipped monitors.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "compatible": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AgentMonitor"
                      }
                    },
                    "skipped": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AgentMonitor"
                      }
                    },
                    "already": {
                      "type": "integer",
                      "description": "Monitors that already check from this agent."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Agents"
        ],
        "summary": "Add the agent to monitors",
        "operationId": "assignAgentMonitors",
        "description": "Adds the agent's location to the given monitors. Monitors that are not compatible are skipped and returned with the reason.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the agent."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "monitor_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer",
                      "format": "int64"
                    }
                  }
                },
                "required": [
                  "monitor_ids"
                ]
              },
              "example": {
                "monitor_ids": [
                  1,
                  4,
                  7
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "added": {
                      "type": "integer"
                    },
                    "skipped": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AgentMonitor"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/auth/2fa/disable": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Disable two-factor",
        "operationId": "disableTwoFactor",
        "description": "**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "TOTP or recovery code."
                  }
                },
                "required": [
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/auth/2fa/enable": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Enable two-factor",
        "operationId": "enableTwoFactor",
        "description": "**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string"
                  }
                },
                "required": [
                  "code"
                ]
              },
              "example": {
                "code": "123456"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Enabled. Recovery codes are shown once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recovery_codes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/auth/2fa/recovery-codes": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "New recovery codes",
        "operationId": "regenerateRecoveryCodes",
        "description": "Replaces the old codes.\n\n**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string"
                  }
                },
                "required": [
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New codes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recovery_codes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/auth/2fa/setup": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Start two-factor setup",
        "operationId": "setupTwoFactor",
        "description": "Returns a new TOTP secret; confirm it with /api/auth/2fa/enable.\n\n**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Secret and otpauth:// URI for the QR code.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "secret": {
                      "type": "string"
                    },
                    "uri": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/auth/identities/{provider}": {
      "delete": {
        "tags": [
          "Auth"
        ],
        "summary": "Unlink a single sign-on provider",
        "operationId": "unlinkIdentity",
        "description": "**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "github",
                "google",
                "oidc"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/auth/login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Log in with password",
        "operationId": "login",
        "description": "Sets the `pingtxt_session` cookie. When the user has two-factor authentication, the response is `{\"two_factor\": true, \"challenge\": \"...\"}` instead: send the challenge with a code to /api/auth/login/2fa.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              },
              "example": {
                "email": "admin@example.com",
                "password": "correct horse"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Logged in, or a two-factor challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/User"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "two_factor": {
                          "const": true
                        },
                        "challenge": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/auth/login/2fa": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Complete a two-factor login",
        "operationId": "loginSecondFactor",
        "description": "**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "challenge": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string",
                    "description": "6 digit TOTP code or a recovery code."
                  }
                },
                "required": [
                  "challenge",
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Logged in, session cookie set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/auth/logout": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Log out",
        "operationId": "logout",
        "description": "Ends the browser session.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/me": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Current user",
        "operationId": "me",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "The user behind the session or token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "version": {
                      "type": "string"
                    },
                    "recovery_codes_left": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "user": {
                    "id": 1,
                    "email": "admin@example.com",
                    "role": "admin",
                    "created_at": 1761868800000,
                    "has_password": true,
                    "two_factor": false,
                    "identities": []
                  },
                  "version": "v0.1.0",
                  "recovery_codes_left": 10
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/auth/oauth/{provider}/callback": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Single sign-on callback",
        "operationId": "oauthCallback",
        "description": "Register `<public url>/api/auth/oauth/<provider>/callback` at the provider. Not meant to be called directly.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "github",
                "google",
                "oidc"
              ]
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the app."
          }
        }
      }
    },
    "/api/auth/oauth/{provider}/start": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Start single sign-on",
        "operationId": "oauthStart",
        "description": "Browser redirect to the provider. `?next=/path` returns there after login, `?link=1` links the provider to the logged-in account.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "github",
                "google",
                "oidc"
              ]
            }
          },
          {
            "name": "next",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Local path to open after login."
          },
          {
            "name": "link",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "1 to link to the current account."
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the provider."
          }
        }
      }
    },
    "/api/auth/options": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Login options",
        "operationId": "authOptions",
        "description": "What the login page shows: password form and single sign-on buttons.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "responses": {
          "200": {
            "description": "Options.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "password_login": {
                      "type": "boolean"
                    },
                    "providers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "password_login": true,
                  "providers": [
                    {
                      "id": "github",
                      "label": "GitHub"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/password": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Change password",
        "operationId": "changePassword",
        "description": "Other sessions of the user are logged out. Accounts created with single sign-on can set a first password with an empty `current`.\n\n**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "current": {
                    "type": "string"
                  },
                  "new": {
                    "type": "string",
                    "description": "At least 8 characters."
                  }
                },
                "required": [
                  "new"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/backups": {
      "get": {
        "tags": [
          "Backups"
        ],
        "summary": "List backups",
        "operationId": "listBackups",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Backups and settings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "settings": {
                      "$ref": "#/components/schemas/BackupSettings"
                    },
                    "backups": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BackupFile"
                      }
                    },
                    "dir": {
                      "type": "string"
                    },
                    "pending_restore": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/PendingRestore"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "driver": {
                      "type": "string",
                      "description": "sqlite, postgres or mysql."
                    },
                    "file_backups": {
                      "type": "boolean",
                      "description": "false on PostgreSQL / MariaDB: use the database's own tools."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Backups"
        ],
        "summary": "Back up now",
        "operationId": "createBackup",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "201": {
            "description": "Backup file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackupFile"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/backups/restore": {
      "delete": {
        "tags": [
          "Backups"
        ],
        "summary": "Cancel a staged restore",
        "operationId": "cancelRestore",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/backups/upload": {
      "post": {
        "tags": [
          "Backups"
        ],
        "summary": "Upload a backup",
        "operationId": "uploadBackup",
        "description": "Raw SQLite file as the body (`Content-Type: application/octet-stream`), up to 4 GB. SQLite only.\n\n**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "backup": {
                      "$ref": "#/components/schemas/BackupFile"
                    },
                    "info": {
                      "$ref": "#/components/schemas/BackupInfo"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/backups/{name}": {
      "get": {
        "tags": [
          "Backups"
        ],
        "summary": "Download a backup",
        "operationId": "downloadBackup",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Backup file name.",
            "example": "pingtxt-manual-20261031-020000.db"
          }
        ],
        "responses": {
          "200": {
            "description": "The file.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Backups"
        ],
        "summary": "Delete a backup",
        "operationId": "deleteBackup",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Backup file name.",
            "example": "pingtxt-manual-20261031-020000.db"
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/backups/{name}/restore": {
      "post": {
        "tags": [
          "Backups"
        ],
        "summary": "Stage a restore",
        "operationId": "stageRestore",
        "description": "The backup replaces the database at the next start of the hub.\n\n**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Backup file name.",
            "example": "pingtxt-manual-20261031-020000.db"
          }
        ],
        "responses": {
          "200": {
            "description": "Staged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pending_restore": {
                      "$ref": "#/components/schemas/PendingRestore"
                    },
                    "info": {
                      "$ref": "#/components/schemas/BackupInfo"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/config/export": {
      "get": {
        "tags": [
          "Backups"
        ],
        "summary": "Export the configuration",
        "operationId": "exportConfig",
        "description": "Admins only. Without secrets by default; `?secrets=1` needs a browser session (API tokens get 403).\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "secrets",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "1 to include passwords and tokens."
          }
        ],
        "responses": {
          "200": {
            "description": "JSON download.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfigDocument"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/config/import": {
      "post": {
        "tags": [
          "Backups"
        ],
        "summary": "Import a configuration",
        "operationId": "importConfig",
        "description": "A backup is taken first. Body up to 64 MB.\n\n**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "merge",
                "replace"
              ],
              "default": "merge"
            },
            "description": "replace deletes what is not in the document."
          },
          {
            "name": "settings",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "0 to leave instance settings alone."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfigDocument"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/import/uptime-kuma": {
      "post": {
        "tags": [
          "Backups"
        ],
        "summary": "Import from Uptime Kuma",
        "operationId": "importUptimeKuma",
        "description": "Creates monitors from an Uptime Kuma 1.x backup file (Settings → Backup → Export in Uptime Kuma). Kuma groups become groups, tags become tags, paused monitors stay paused. Monitors whose name already exists are left alone; unsupported types are skipped with a reason. Notification channels are not imported. With `dry_run=1` nothing is created and the answer is the preview.\n\n**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "dry_run",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Preview only."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "The Uptime Kuma backup file.",
                "properties": {
                  "version": {
                    "type": "string"
                  },
                  "monitorList": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  }
                },
                "required": [
                  "monitorList"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was (or would be) done with each monitor.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "string"
                    },
                    "monitors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "kuma_type": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string"
                          },
                          "target": {
                            "type": "string"
                          },
                          "group": {
                            "type": "string"
                          },
                          "paused": {
                            "type": "boolean"
                          },
                          "action": {
                            "type": "string",
                            "enum": [
                              "create",
                              "exists",
                              "skip"
                            ]
                          },
                          "reason": {
                            "type": "string"
                          },
                          "note": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "groups": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "created": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/docs": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Interactive API docs",
        "operationId": "apiDocs",
        "description": "Redirects to the Swagger UI page of the web app.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "responses": {
          "302": {
            "description": "Redirect to /api-docs/."
          }
        }
      }
    },
    "/api/groups": {
      "get": {
        "tags": [
          "Groups and tags"
        ],
        "summary": "List groups",
        "operationId": "listGroups",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Groups.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Group"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Groups and tags"
        ],
        "summary": "Create a group",
        "operationId": "createGroup",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Group"
              },
              "example": {
                "name": "Production",
                "description": "Customer facing"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/groups/{id}": {
      "put": {
        "tags": [
          "Groups and tags"
        ],
        "summary": "Update a group",
        "operationId": "updateGroup",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the group."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Group"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Groups and tags"
        ],
        "summary": "Delete a group",
        "operationId": "deleteGroup",
        "description": "Its monitors are kept, without a group.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the group."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/groups/{id}/monitors": {
      "put": {
        "tags": [
          "Groups and tags"
        ],
        "summary": "Set the monitors of a group",
        "operationId": "setGroupMonitors",
        "description": "Monitors listed here move to this group; monitors of the group not listed leave it.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the group."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "monitor_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer",
                      "format": "int64"
                    }
                  }
                },
                "required": [
                  "monitor_ids"
                ]
              },
              "example": {
                "monitor_ids": [
                  7,
                  8
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Group.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Health check",
        "operationId": "health",
        "description": "Liveness probe, also returns the hub version.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "responses": {
          "200": {
            "description": "Hub is running.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "version": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "version": "v0.1.0"
                }
              }
            }
          }
        }
      }
    },
    "/api/incidents": {
      "get": {
        "tags": [
          "Incidents"
        ],
        "summary": "List incidents",
        "operationId": "listIncidents",
        "description": "The latest 100, open ones first.\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "monitor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Only this monitor."
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "resolved"
              ]
            },
            "description": "Only open or only resolved incidents."
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Only incidents open at some point after this time (unix ms)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            },
            "description": "Maximum number of incidents."
          }
        ],
        "responses": {
          "200": {
            "description": "Incidents.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Incident"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/locations": {
      "get": {
        "tags": [
          "Agents"
        ],
        "summary": "List check locations",
        "operationId": "listLocations",
        "description": "The hub (when it runs checks) and every agent.\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Locations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Location"
                  }
                },
                "example": [
                  {
                    "id": "local",
                    "name": "hub",
                    "online": true,
                    "kind": "local",
                    "ipv6": 2
                  },
                  {
                    "id": "agent:2",
                    "name": "fra-1 · Frankfurt",
                    "online": true,
                    "kind": "agent",
                    "ipv6": 1
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/maintenances": {
      "get": {
        "tags": [
          "Maintenance"
        ],
        "summary": "List maintenance windows",
        "operationId": "listMaintenances",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Windows.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Maintenance"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Maintenance"
        ],
        "summary": "Create a maintenance window",
        "operationId": "createMaintenance",
        "description": "Covered monitors don't alert and don't count as down.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MaintenanceInput"
              },
              "example": {
                "name": "DB upgrade",
                "description": "",
                "all_monitors": false,
                "monitor_ids": [
                  7
                ],
                "group_ids": [],
                "tag_ids": [],
                "kind": "weekly",
                "start_time": "02:00",
                "duration_min": 60,
                "weekdays": [
                  0
                ],
                "month_days": [],
                "timezone": "Europe/Rome",
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Maintenance"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/maintenances/{id}": {
      "put": {
        "tags": [
          "Maintenance"
        ],
        "summary": "Update a maintenance window",
        "operationId": "updateMaintenance",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the maintenance window."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MaintenanceInput"
              },
              "example": {
                "name": "DB upgrade",
                "description": "",
                "all_monitors": false,
                "monitor_ids": [
                  7
                ],
                "group_ids": [],
                "tag_ids": [],
                "kind": "weekly",
                "start_time": "02:00",
                "duration_min": 60,
                "weekdays": [
                  0
                ],
                "month_days": [],
                "timezone": "Europe/Rome",
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Maintenance"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Maintenance"
        ],
        "summary": "Delete a maintenance window",
        "operationId": "deleteMaintenance",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the maintenance window."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/monitors": {
      "get": {
        "tags": [
          "Monitors"
        ],
        "summary": "List monitors",
        "operationId": "listMonitors",
        "description": "Every monitor with its last 24 hours. Secrets (passwords in targets, headers, bodies, push tokens) are hidden from read-only users.\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "group",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Group id, or `none` for monitors without a group."
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Tag id."
          }
        ],
        "responses": {
          "200": {
            "description": "Monitors.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MonitorSummary"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Monitors"
        ],
        "summary": "Create a monitor",
        "operationId": "createMonitor",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MonitorInput"
              },
              "example": {
                "name": "Website",
                "type": "http",
                "target": "https://example.com",
                "interval_sec": 60,
                "timeout_ms": 10000,
                "retries": 2,
                "method": "GET",
                "expected_status": "200-299",
                "locations": [
                  "local"
                ],
                "tag_ids": [],
                "group_id": 0,
                "assertions": [
                  {
                    "path": "status",
                    "op": "eq",
                    "value": "ok"
                  }
                ],
                "cert_expiry_alert": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitor"
                },
                "example": {
                  "name": "Website",
                  "type": "http",
                  "target": "https://example.com",
                  "interval_sec": 60,
                  "timeout_ms": 10000,
                  "retries": 2,
                  "method": "GET",
                  "expected_status": "200-299",
                  "locations": [
                    "local"
                  ],
                  "tag_ids": [],
                  "group_id": 0,
                  "assertions": [
                    {
                      "path": "status",
                      "op": "eq",
                      "value": "ok"
                    }
                  ],
                  "cert_expiry_alert": true,
                  "id": 7,
                  "headers": {},
                  "body": "",
                  "keyword": "",
                  "keyword_invert": false,
                  "dns_record": "",
                  "dns_server": "",
                  "dns_expected": "",
                  "ignore_tls": false,
                  "push_token": "",
                  "grace_sec": 0,
                  "degraded_pct": 0,
                  "ip_family": 0,
                  "domain_expiry_alert": false,
                  "paused": false,
                  "status": "up",
                  "status_since": 1761868800000,
                  "created_at": 1761868800000
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/monitors/test": {
      "post": {
        "tags": [
          "Monitors"
        ],
        "summary": "Test an unsaved monitor",
        "operationId": "testDraftMonitor",
        "description": "Runs the check once at each location of the monitor in the body and waits for the results (up to the timeout plus 5 seconds). Nothing is saved. Heartbeat (push) monitors cannot be tested.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MonitorInput"
              },
              "example": {
                "type": "http",
                "target": "https://example.com/health",
                "timeout_ms": 10000,
                "locations": [
                  "local"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per location of the monitor. Nothing is stored and no alert is sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TestResult"
                      }
                    }
                  }
                },
                "example": {
                  "results": [
                    {
                      "location": "local",
                      "name": "hub",
                      "ok": true,
                      "latency_ms": 84.2,
                      "status_code": 200
                    },
                    {
                      "location": "agent:2",
                      "name": "fra · Frankfurt",
                      "ok": false,
                      "skipped": true,
                      "latency_ms": 0,
                      "message": "agent offline"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/monitors/bulk": {
      "post": {
        "tags": [
          "Monitors"
        ],
        "summary": "Change several monitors",
        "operationId": "bulkMonitors",
        "description": "Pauses, resumes, deletes or moves to a group several monitors at once. Pausing resolves their open incidents. With action group, group_id 0 removes the monitors from their group.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ids": {
                    "type": "array",
                    "items": {
                      "type": "integer",
                      "format": "int64"
                    }
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "pause",
                      "resume",
                      "delete",
                      "group"
                    ]
                  },
                  "group_id": {
                    "type": "integer",
                    "format": "int64",
                    "description": "Action group only: the group, 0 for none."
                  }
                },
                "required": [
                  "ids",
                  "action"
                ]
              },
              "example": {
                "ids": [
                  1,
                  2,
                  3
                ],
                "action": "pause"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Number of monitors selected.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer"
                    }
                  }
                },
                "example": {
                  "count": 3
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/monitors/{id}": {
      "get": {
        "tags": [
          "Monitors"
        ],
        "summary": "Get a monitor",
        "operationId": "getMonitor",
        "description": "Includes 7 and 30 day uptime and the last result of each location.\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the monitor."
          }
        ],
        "responses": {
          "200": {
            "description": "Monitor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MonitorDetail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Monitors"
        ],
        "summary": "Update a monitor",
        "operationId": "updateMonitor",
        "description": "Replaces the monitor: send every field.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the monitor."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MonitorInput"
              },
              "example": {
                "name": "Website",
                "type": "http",
                "target": "https://example.com",
                "interval_sec": 60,
                "timeout_ms": 10000,
                "retries": 2,
                "method": "GET",
                "expected_status": "200-299",
                "locations": [
                  "local"
                ],
                "tag_ids": [],
                "group_id": 0,
                "assertions": [
                  {
                    "path": "status",
                    "op": "eq",
                    "value": "ok"
                  }
                ],
                "cert_expiry_alert": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitor"
                },
                "example": {
                  "name": "Website",
                  "type": "http",
                  "target": "https://example.com",
                  "interval_sec": 60,
                  "timeout_ms": 10000,
                  "retries": 2,
                  "method": "GET",
                  "expected_status": "200-299",
                  "locations": [
                    "local"
                  ],
                  "tag_ids": [],
                  "group_id": 0,
                  "assertions": [
                    {
                      "path": "status",
                      "op": "eq",
                      "value": "ok"
                    }
                  ],
                  "cert_expiry_alert": true,
                  "id": 7,
                  "headers": {},
                  "body": "",
                  "keyword": "",
                  "keyword_invert": false,
                  "dns_record": "",
                  "dns_server": "",
                  "dns_expected": "",
                  "ignore_tls": false,
                  "push_token": "",
                  "grace_sec": 0,
                  "degraded_pct": 0,
                  "ip_family": 0,
                  "domain_expiry_alert": false,
                  "paused": false,
                  "status": "up",
                  "status_since": 1761868800000,
                  "created_at": 1761868800000
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Monitors"
        ],
        "summary": "Delete a monitor",
        "operationId": "deleteMonitor",
        "description": "Also deletes its history.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the monitor."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/monitors/{id}/expiry": {
      "post": {
        "tags": [
          "Monitors"
        ],
        "summary": "Check expiry dates now",
        "operationId": "checkMonitorExpiry",
        "description": "Looks up the TLS certificate and domain expiry.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the monitor."
          }
        ],
        "responses": {
          "200": {
            "description": "Expiry checks.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ExpiryCheck"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/monitors/{id}/pause": {
      "post": {
        "tags": [
          "Monitors"
        ],
        "summary": "Pause or resume",
        "operationId": "pauseMonitor",
        "description": "Pausing resolves the open incident.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the monitor."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "paused": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "paused"
                ]
              },
              "example": {
                "paused": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Monitor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitor"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/monitors/{id}/test": {
      "post": {
        "tags": [
          "Monitors"
        ],
        "summary": "Check now",
        "operationId": "testMonitor",
        "description": "Runs the saved monitor's check once at each of its locations and returns the results. They are not stored and do not change the status.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the monitor."
          }
        ],
        "responses": {
          "200": {
            "description": "One result per location of the monitor. Nothing is stored and no alert is sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TestResult"
                      }
                    }
                  }
                },
                "example": {
                  "results": [
                    {
                      "location": "local",
                      "name": "hub",
                      "ok": true,
                      "latency_ms": 84.2,
                      "status_code": 200
                    },
                    {
                      "location": "agent:2",
                      "name": "fra · Frankfurt",
                      "ok": false,
                      "skipped": true,
                      "latency_ms": 0,
                      "message": "agent offline"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/monitors/{id}/push-token": {
      "post": {
        "tags": [
          "Monitors"
        ],
        "summary": "Rotate the heartbeat URL",
        "operationId": "rotatePushToken",
        "description": "push monitors only: the old URL stops working.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the monitor."
          }
        ],
        "responses": {
          "200": {
            "description": "Monitor with the new push_token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Monitor"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/monitors/{id}/results": {
      "get": {
        "tags": [
          "Monitors"
        ],
        "summary": "Latest check results",
        "operationId": "monitorResults",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the monitor."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 50
            },
            "description": "Number of results."
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Only results older than this time (unix ms), to load the next page."
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Only results at or after this time (unix ms)."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Only results before this time (unix ms)."
          },
          {
            "name": "failed",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Only failed checks."
          }
        ],
        "responses": {
          "200": {
            "description": "Results, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Result"
                      }
                    },
                    "locations": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "description": "Location id → display name."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/monitors/{id}/series": {
      "get": {
        "tags": [
          "Monitors"
        ],
        "summary": "Response time series",
        "operationId": "monitorSeries",
        "description": "About 90 buckets per location over the range, or over a custom window with `start` and `end`. `stats` sums up the window and the one of the same length just before it.\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the monitor."
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1h",
                "6h",
                "24h",
                "7d",
                "30d"
              ],
              "default": "24h"
            },
            "description": "Time range."
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Custom window start, unix ms. With `end`, replaces `range`; 5 minutes to 400 days long."
          },
          {
            "name": "end",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Custom window end, unix ms (capped at now)."
          }
        ],
        "responses": {
          "200": {
            "description": "Series.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "integer",
                      "format": "int64",
                      "description": "Unix time in milliseconds (0 = never / unknown)."
                    },
                    "to": {
                      "type": "integer",
                      "format": "int64",
                      "description": "Unix time in milliseconds (0 = never / unknown)."
                    },
                    "size": {
                      "type": "integer",
                      "format": "int64",
                      "description": "Bucket size in ms."
                    },
                    "locations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "buckets": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Bucket"
                      }
                    },
                    "stats": {
                      "type": "object",
                      "properties": {
                        "current": {
                          "type": "object",
                          "properties": {
                            "from": {
                              "type": "integer",
                              "format": "int64"
                            },
                            "to": {
                              "type": "integer",
                              "format": "int64"
                            },
                            "uptime": {
                              "type": "number",
                              "description": "0..1, -1 = no checks."
                            },
                            "avg_ms": {
                              "type": "number",
                              "description": "Average latency of the successful checks."
                            },
                            "percentiles": {
                              "type": "object",
                              "description": "Latency percentiles of the successful checks, from single results only: older history is kept as hourly summaries.",
                              "properties": {
                                "n": {
                                  "type": "integer",
                                  "description": "Checks counted (0 = none)."
                                },
                                "from": {
                                  "type": "integer",
                                  "format": "int64",
                                  "description": "Start of the checks counted: later than the window start when older single results were pruned."
                                },
                                "p50": {
                                  "type": "number",
                                  "description": "Median, ms."
                                },
                                "p95": {
                                  "type": "number",
                                  "description": "ms."
                                },
                                "p99": {
                                  "type": "number",
                                  "description": "ms."
                                }
                              }
                            }
                          }
                        },
                        "previous": {
                          "type": "object",
                          "properties": {
                            "from": {
                              "type": "integer",
                              "format": "int64"
                            },
                            "to": {
                              "type": "integer",
                              "format": "int64"
                            },
                            "uptime": {
                              "type": "number",
                              "description": "0..1, -1 = no checks."
                            },
                            "avg_ms": {
                              "type": "number",
                              "description": "Average latency of the successful checks."
                            },
                            "percentiles": {
                              "type": "object",
                              "description": "Latency percentiles of the successful checks, from single results only: older history is kept as hourly summaries.",
                              "properties": {
                                "n": {
                                  "type": "integer",
                                  "description": "Checks counted (0 = none)."
                                },
                                "from": {
                                  "type": "integer",
                                  "format": "int64",
                                  "description": "Start of the checks counted: later than the window start when older single results were pruned."
                                },
                                "p50": {
                                  "type": "number",
                                  "description": "Median, ms."
                                },
                                "p95": {
                                  "type": "number",
                                  "description": "ms."
                                },
                                "p99": {
                                  "type": "number",
                                  "description": "ms."
                                }
                              }
                            }
                          },
                          "description": "The window of the same length just before."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/notifiers": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "List notification channels",
        "operationId": "listNotifiers",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Channels.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Notifier"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Notifications"
        ],
        "summary": "Create a notification channel",
        "operationId": "createNotifier",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.\n\nAn enabled `app` channel needs push notifications on in Settings, Mobile app: 409 otherwise.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Notifier"
              },
              "example": {
                "name": "ops",
                "type": "slack",
                "config": {
                  "url": "https://hooks.slack.com/services/T000/B000/XXXX"
                },
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Notifier"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/notifiers/test": {
      "post": {
        "tags": [
          "Notifications"
        ],
        "summary": "Test an unsaved channel",
        "operationId": "testDraftNotifier",
        "description": "Sends a test alert with this configuration. With an `id`, empty secrets are taken from that saved channel.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Notifier"
              },
              "example": {
                "name": "ops",
                "type": "slack",
                "config": {
                  "url": "https://hooks.slack.com/services/T000/B000/XXXX"
                },
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/notifiers/{id}": {
      "put": {
        "tags": [
          "Notifications"
        ],
        "summary": "Update a notification channel",
        "operationId": "updateNotifier",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.\n\nAn enabled `app` channel needs push notifications on in Settings, Mobile app: 409 otherwise.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the channel."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Notifier"
              },
              "example": {
                "name": "ops",
                "type": "slack",
                "config": {
                  "url": "https://hooks.slack.com/services/T000/B000/XXXX"
                },
                "enabled": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Notifier"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Notifications"
        ],
        "summary": "Delete a notification channel",
        "operationId": "deleteNotifier",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the channel."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/notifiers/{id}/test": {
      "post": {
        "tags": [
          "Notifications"
        ],
        "summary": "Test a channel",
        "operationId": "testNotifier",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the channel."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/oauth/grants": {
      "get": {
        "tags": [
          "API tokens"
        ],
        "summary": "List connected apps",
        "operationId": "listGrants",
        "description": "Apps connected through OAuth (the iOS app, MCP clients): one entry per sign-in.\n\n**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Grants.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/OAuthGrant"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/oauth/grants/{id}": {
      "delete": {
        "tags": [
          "API tokens"
        ],
        "summary": "Disconnect an app",
        "operationId": "deleteGrant",
        "description": "**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the grant."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/openapi.json": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "OpenAPI document",
        "operationId": "openapi",
        "description": "This document, for the running version.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/overview": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Dashboard counters",
        "operationId": "getOverview",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Counters.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "monitors": {
                      "type": "integer"
                    },
                    "status": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      },
                      "description": "Monitors per status."
                    },
                    "agents": {
                      "type": "integer"
                    },
                    "agents_online": {
                      "type": "integer"
                    },
                    "open_incidents": {
                      "type": "integer"
                    },
                    "version": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "monitors": 12,
                  "status": {
                    "up": 10,
                    "down": 1,
                    "paused": 1
                  },
                  "agents": 2,
                  "agents_online": 2,
                  "open_incidents": 1,
                  "version": "v0.1.0"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/version": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Version and update check",
        "operationId": "getVersion",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).\n\nThe hub asks GitHub for the latest release every 12 hours (release builds only, unless turned off in the settings) and reports whether a newer version is out.",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Version status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/version/check": {
      "post": {
        "tags": [
          "System"
        ],
        "summary": "Check for a new version now",
        "operationId": "checkVersion",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.\n\nAsks GitHub right away (at most once a minute; a newer call returns the last answer).",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Version status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "description": "The check is off (settings, or a development build).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/telemetry": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Anonymous usage stats",
        "operationId": "getTelemetry",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.\n\nWhether an admin answered the opt-in question, whether stats are on, and the exact report sent once an hour (random instance id, version, active monitors, connected agents).",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Usage stats status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelemetryStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "put": {
        "tags": [
          "System"
        ],
        "summary": "Turn anonymous usage stats on or off",
        "operationId": "setTelemetry",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.\n\nRecords the admin's answer. The instance id is made the first time stats are turned on and never changes.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Usage stats status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelemetryStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "description": "Usage stats are turned off on this hub (PINGTXT_DISABLE_TELEMETRY or a development build)."
          }
        }
      }
    },
    "/api/public/status/{slug}": {
      "get": {
        "tags": [
          "Status pages"
        ],
        "summary": "Public status page data",
        "operationId": "publicStatus",
        "description": "What /status/<slug> shows. Drafts answer 404 unless a user is logged in. Cached for 15 seconds.\n\nPublished pages without a password answer with `Access-Control-Allow-Origin: *`, so any website can read them from the browser (for example to show a status dot).\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicStatusPage"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "403": {
            "description": "The page needs a password: unlock it with `POST /api/public/status/{slug}/unlock`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "password_required": {
                      "type": "boolean"
                    },
                    "title": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "error": "this status page needs a password",
                  "password_required": true,
                  "title": "Acme status"
                }
              }
            }
          }
        }
      }
    },
    "/api/badge/{slug}/{kind}": {
      "get": {
        "tags": [
          "Status pages"
        ],
        "summary": "Status page badge",
        "operationId": "pageBadge",
        "description": "SVG badge with the overall state of a status page (operational, degraded, partial outage, major outage), to embed in a README:\n\n```markdown\n![status](https://pingtxt.example.com/api/badge/acme/status.svg)\n```\n\n**Access:** No authentication. Only published status pages and the monitors they show have badges; anything else answers the same 404 badge. Also served on the page's custom domain. Cached for 60 seconds.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Slug of a published status page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "path",
            "required": true,
            "description": "`status`, optionally with `.svg`.",
            "schema": {
              "type": "string",
              "enum": [
                "status",
                "status.svg"
              ]
            }
          },
          {
            "name": "label",
            "in": "query",
            "description": "Replaces the left text (up to 60 characters).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "style",
            "in": "query",
            "description": "`flat` (default) or `flat-square`.",
            "schema": {
              "type": "string",
              "enum": [
                "flat",
                "flat-square"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "SVG badge.",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Not found badge.",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/badge/{slug}/{monitor}/{kind}": {
      "get": {
        "tags": [
          "Status pages"
        ],
        "summary": "Monitor badge",
        "operationId": "monitorBadge",
        "description": "SVG badge of one monitor shown on a published status page: its state (`status`), its uptime (`uptime`) or its average response time (`response`) over `period`. The label defaults to the monitor name, `uptime 30d` or `response 24h`.\n\n```markdown\n![uptime](https://pingtxt.example.com/api/badge/acme/12/uptime.svg?period=7d)\n```\n\n**Access:** No authentication. Only published status pages and the monitors they show have badges; anything else answers the same 404 badge. Also served on the page's custom domain. Cached for 60 seconds.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Slug of a published status page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "monitor",
            "in": "path",
            "required": true,
            "description": "Monitor id.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "kind",
            "in": "path",
            "required": true,
            "description": "`status`, `uptime` or `response`, optionally with `.svg`.",
            "schema": {
              "type": "string",
              "enum": [
                "status",
                "uptime",
                "response",
                "status.svg",
                "uptime.svg",
                "response.svg"
              ]
            }
          },
          {
            "name": "period",
            "in": "query",
            "description": "Window for `uptime` (default 30d) and `response` (default 24h).",
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "30d",
                "90d"
              ]
            }
          },
          {
            "name": "label",
            "in": "query",
            "description": "Replaces the left text (up to 60 characters).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "style",
            "in": "query",
            "description": "`flat` (default) or `flat-square`.",
            "schema": {
              "type": "string",
              "enum": [
                "flat",
                "flat-square"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "SVG badge.",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Bad period badge.",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Not found badge.",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/push/{token}": {
      "get": {
        "tags": [
          "Monitors"
        ],
        "summary": "Send a heartbeat",
        "operationId": "pushGet",
        "description": "Heartbeat of a push monitor. Any HTTP method works (GET and POST shown here). The token in the URL is the authentication.\n\n`curl -fsS https://status.example.com/api/push/<token>?status=up&msg=ok&ping=12`\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "up",
                "down",
                "ok",
                "fail",
                "0",
                "1"
              ],
              "default": "up"
            },
            "description": "Result of the job."
          },
          {
            "name": "msg",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Message shown in the history, up to 200 characters."
          },
          {
            "name": "ping",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "Duration or latency in ms."
          }
        ],
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "paused": {
                      "type": "boolean",
                      "description": "The monitor is paused: the heartbeat was ignored."
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Monitors"
        ],
        "summary": "Send a heartbeat",
        "operationId": "pushPost",
        "description": "Heartbeat of a push monitor. Any HTTP method works (GET and POST shown here). The token in the URL is the authentication.\n\n`curl -fsS https://status.example.com/api/push/<token>?status=up&msg=ok&ping=12`\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "up",
                "down",
                "ok",
                "fail",
                "0",
                "1"
              ],
              "default": "up"
            },
            "description": "Result of the job."
          },
          {
            "name": "msg",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Message shown in the history, up to 200 characters."
          },
          {
            "name": "ping",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "Duration or latency in ms."
          }
        ],
        "responses": {
          "200": {
            "description": "Recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "paused": {
                      "type": "boolean",
                      "description": "The monitor is paused: the heartbeat was ignored."
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/settings": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get instance settings",
        "operationId": "getSettings",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Settings"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/settings/agent-updates": {
      "put": {
        "tags": [
          "Agents"
        ],
        "summary": "Agent auto-update setting",
        "operationId": "putAgentUpdateSettings",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "auto_update": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "auto_update"
                ]
              },
              "example": {
                "auto_update": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auto_update": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/settings/auth": {
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Save login settings",
        "operationId": "putAuthSettings",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AuthSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Settings"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/settings/backups": {
      "put": {
        "tags": [
          "Backups"
        ],
        "summary": "Save backup schedule",
        "operationId": "putBackupSettings",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BackupSettings"
              },
              "example": {
                "enabled": true,
                "interval_hours": 24,
                "keep": 7
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackupSettings"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/settings/general": {
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Save general settings",
        "operationId": "putGeneralSettings",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GeneralSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Settings"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/setup": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Setup state",
        "operationId": "getSetup",
        "description": "Whether the instance still needs its first admin account.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "responses": {
          "200": {
            "description": "State.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "needs_setup": {
                      "type": "boolean"
                    },
                    "version": {
                      "type": "string"
                    }
                  }
                },
                "example": {
                  "needs_setup": false,
                  "version": "v0.1.0"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Create the first admin",
        "operationId": "setup",
        "description": "Works only while no user exists. Starts a session.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string",
                    "description": "At least 8 characters."
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              },
              "example": {
                "email": "admin@example.com",
                "password": "correct horse"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Admin created, session cookie set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                },
                "example": {
                  "id": 1,
                  "email": "admin@example.com",
                  "role": "admin",
                  "created_at": 1761868800000,
                  "has_password": true,
                  "two_factor": false,
                  "identities": []
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/status-pages": {
      "get": {
        "tags": [
          "Status pages"
        ],
        "summary": "List status pages",
        "operationId": "listStatusPages",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Pages.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "pages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StatusPage"
                      }
                    },
                    "hub_url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Status pages"
        ],
        "summary": "Create a status page",
        "operationId": "createStatusPage",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StatusPage"
              },
              "example": {
                "slug": "acme",
                "title": "Acme status",
                "description": "",
                "domain": "",
                "published": true,
                "show_incidents": true,
                "monitor_ids": [
                  7
                ],
                "group_ids": []
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/status-pages/preview": {
      "post": {
        "tags": [
          "Status pages"
        ],
        "summary": "Preview a status page",
        "operationId": "previewStatusPage",
        "description": "What visitors would see with these settings, without saving them. With the `id` of a saved page, the incidents written on it are included.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StatusPage"
              },
              "example": {
                "slug": "acme",
                "title": "Acme status",
                "description": "",
                "domain": "",
                "published": true,
                "show_incidents": true,
                "monitor_ids": [
                  7
                ],
                "group_ids": []
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicStatusPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/status-pages/{id}": {
      "get": {
        "tags": [
          "Status pages"
        ],
        "summary": "Get a status page",
        "operationId": "getStatusPage",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          }
        ],
        "responses": {
          "200": {
            "description": "Page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Status pages"
        ],
        "summary": "Update a status page",
        "operationId": "updateStatusPage",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StatusPage"
              },
              "example": {
                "slug": "acme",
                "title": "Acme status",
                "description": "",
                "domain": "",
                "published": true,
                "show_incidents": true,
                "monitor_ids": [
                  7
                ],
                "group_ids": []
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Status pages"
        ],
        "summary": "Delete a status page",
        "operationId": "deleteStatusPage",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/tags": {
      "get": {
        "tags": [
          "Groups and tags"
        ],
        "summary": "List tags",
        "operationId": "listTags",
        "description": "**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Tags.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Tag"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Groups and tags"
        ],
        "summary": "Create a tag",
        "operationId": "createTag",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Tag"
              },
              "example": {
                "name": "critical",
                "color": "red"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tag"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/tags/{id}": {
      "put": {
        "tags": [
          "Groups and tags"
        ],
        "summary": "Update a tag",
        "operationId": "updateTag",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the tag."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Tag"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tag"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Groups and tags"
        ],
        "summary": "Delete a tag",
        "operationId": "deleteTag",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the tag."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/tokens": {
      "get": {
        "tags": [
          "API tokens"
        ],
        "summary": "List your API tokens",
        "operationId": "listTokens",
        "description": "**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Tokens of the current user.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/APIToken"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "API tokens"
        ],
        "summary": "Create an API token",
        "operationId": "createToken",
        "description": "The secret is returned once. Read-only users can only create read tokens. At most 50 tokens per user.\n\n**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Up to 100 characters."
                  },
                  "scope": {
                    "type": "string",
                    "enum": [
                      "read",
                      "write"
                    ]
                  },
                  "expires_in_days": {
                    "type": "integer",
                    "description": "0 = never, up to 3650."
                  }
                },
                "required": [
                  "name",
                  "scope"
                ]
              },
              "example": {
                "name": "grafana",
                "scope": "read",
                "expires_in_days": 365
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "$ref": "#/components/schemas/APIToken"
                    },
                    "secret": {
                      "type": "string",
                      "description": "Send it as `Authorization: Bearer <secret>`."
                    }
                  }
                },
                "example": {
                  "token": {
                    "id": 3,
                    "name": "grafana",
                    "hint": "x9Qa",
                    "scope": "read",
                    "created_at": 1761868800000,
                    "last_used_at": 0,
                    "expires_at": 1793404800000
                  },
                  "secret": "ptk_5uG...x9Qa"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/tokens/{id}": {
      "delete": {
        "tags": [
          "API tokens"
        ],
        "summary": "Revoke an API token",
        "operationId": "deleteToken",
        "description": "**Access:** Browser session only: API tokens get 403.",
        "x-pingtxt-access": "session",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the token."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/users": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "List users",
        "operationId": "listUsers",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Users.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/User"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Create a user",
        "operationId": "createUser",
        "description": "Without a password the user logs in with single sign-on (matched by verified email).\n\n**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string",
                    "description": "Optional, at least 8 characters."
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "viewer"
                    ]
                  }
                },
                "required": [
                  "email",
                  "role"
                ]
              },
              "example": {
                "email": "dev@example.com",
                "role": "viewer"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/users/{id}": {
      "put": {
        "tags": [
          "Users"
        ],
        "summary": "Change a user's role",
        "operationId": "updateUser",
        "description": "You cannot change your own role.\n\n**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the user."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "viewer"
                    ]
                  }
                },
                "required": [
                  "role"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Users"
        ],
        "summary": "Delete a user",
        "operationId": "deleteUser",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the user."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/users/{id}/2fa": {
      "delete": {
        "tags": [
          "Users"
        ],
        "summary": "Reset a user's two-factor",
        "operationId": "resetUserTwoFactor",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the user."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/metrics": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Prometheus metrics",
        "operationId": "getMetrics",
        "description": "Metrics in the Prometheus text format. Public when *settings → general → metrics* is set to public.\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Metrics.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "pingtxt_monitor_up{monitor=\"Website\",type=\"http\"} 1\n"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/reports": {
      "get": {
        "tags": [
          "Reports"
        ],
        "summary": "Get report settings",
        "operationId": "getReports",
        "description": "Report settings, when the next reports go out and the last ones sent.\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Reports"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "put": {
        "tags": [
          "Reports"
        ],
        "summary": "Save report settings",
        "operationId": "putReports",
        "description": "Turns the weekly and monthly reports on or off and picks the email channels, the groups, the hour and the time zone. A report that is turned on starts with the next period.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReportSettings"
              },
              "example": {
                "weekly": true,
                "monthly": true,
                "hour": 8,
                "timezone": "Europe/Rome",
                "notifier_ids": [
                  3
                ],
                "group_ids": []
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Reports"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/reports/preview": {
      "get": {
        "tags": [
          "Reports"
        ],
        "summary": "Preview a report",
        "operationId": "previewReport",
        "description": "The report of the last complete week (Monday to Sunday) or month, with the saved settings, as it would be sent now. `text` is the plain text body of the email.\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "weekly",
                "monthly"
              ],
              "default": "weekly"
            },
            "description": "Which report."
          }
        ],
        "responses": {
          "200": {
            "description": "Report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Report"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/reports/send": {
      "post": {
        "tags": [
          "Reports"
        ],
        "summary": "Send a report now",
        "operationId": "sendReport",
        "description": "Emails the report of the last complete period to the saved channels now. The schedule does not change.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "weekly",
                      "monthly"
                    ],
                    "default": "weekly"
                  }
                }
              },
              "example": {
                "kind": "monthly"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent to at least one channel.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "sent": {
                      "type": "integer",
                      "description": "Channels that accepted the email."
                    },
                    "error": {
                      "type": "string",
                      "description": "Channels that failed, if any."
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "sent": 1
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/public/status/{slug}/subscribe": {
      "post": {
        "tags": [
          "Status pages"
        ],
        "summary": "Subscribe to a status page by email",
        "operationId": "publicSubscribe",
        "description": "Sends a confirmation link to the address; nothing else is sent until it is confirmed. The answer is the same for new, waiting and already confirmed addresses. On a custom domain the path is `/api/public/status/subscribe`. Limited to 10 requests per client address and 3 per email address every 15 minutes.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  }
                },
                "required": [
                  "email"
                ]
              },
              "example": {
                "email": "jane@example.com"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Confirmation email queued (or already subscribed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/public/status/{slug}/unlock": {
      "post": {
        "tags": [
          "Status pages"
        ],
        "summary": "Unlock a password protected status page",
        "operationId": "publicUnlock",
        "description": "Checks the page password and sets a cookie that opens the page, its badges and its subscription form for 30 days. Changing or removing the password closes the page again for every visitor. On a custom domain the path is `/api/public/status/unlock`. Limited to 10 wrong passwords per client address and 100 per page every 15 minutes.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Unlocked (or the page has no password); the cookie is set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "description": "Wrong password.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/api/public/subscriptions/{token}/confirm": {
      "post": {
        "tags": [
          "Status pages"
        ],
        "summary": "Confirm an email subscription",
        "operationId": "confirmSubscription",
        "description": "Called by the status page when a visitor opens the link of the confirmation email. Links expire after 7 days.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Token of the links sent by email."
          }
        ],
        "responses": {
          "200": {
            "description": "Confirmed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "email": {
                      "type": "string"
                    },
                    "page": {
                      "type": "string",
                      "description": "Title of the status page."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/subscriptions/{token}/unsubscribe": {
      "post": {
        "tags": [
          "Status pages"
        ],
        "summary": "Unsubscribe from a status page",
        "operationId": "unsubscribe",
        "description": "Removes the address. Also the target of the `List-Unsubscribe` header of update emails (RFC 8058 one-click: the body may be a form, not JSON).\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Token of the links sent by email."
          }
        ],
        "responses": {
          "200": {
            "description": "Unsubscribed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "email": {
                      "type": "string"
                    },
                    "page": {
                      "type": "string",
                      "description": "Title of the status page."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/status-pages/{id}/incidents": {
      "get": {
        "tags": [
          "Status pages"
        ],
        "summary": "List the incidents of a status page",
        "operationId": "listPageIncidents",
        "description": "Incidents written on the page, open ones first, each with its updates (newest first).\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          }
        ],
        "responses": {
          "200": {
            "description": "Incidents.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incidents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PageIncident"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "tags": [
          "Status pages"
        ],
        "summary": "Create an incident on a status page",
        "operationId": "createPageIncident",
        "description": "Opens an incident with its first update. With `notify` (default true) the update is emailed to the confirmed subscribers of the page, in the background.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageIncidentInput"
              },
              "example": {
                "title": "Slow API responses",
                "impact": "minor",
                "status": "investigating",
                "message": "We are looking into slow responses of the API.",
                "notify": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incident": {
                      "$ref": "#/components/schemas/PageIncident"
                    },
                    "notified": {
                      "type": "integer",
                      "description": "Emails queued for subscribers."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/status-pages/{id}/incidents/{incident}": {
      "put": {
        "tags": [
          "Status pages"
        ],
        "summary": "Edit an incident",
        "operationId": "updatePageIncident",
        "description": "Changes the title and impact. Updates are added with POST …/updates.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          },
          {
            "name": "incident",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the incident."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageIncidentInput"
              },
              "example": {
                "title": "Slow API responses in EU",
                "impact": "major"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Incident.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PageIncident"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Status pages"
        ],
        "summary": "Delete an incident",
        "operationId": "deletePageIncident",
        "description": "Removes the incident and all its updates from the page.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          },
          {
            "name": "incident",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the incident."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/status-pages/{id}/incidents/{incident}/updates": {
      "post": {
        "tags": [
          "Status pages"
        ],
        "summary": "Post an incident update",
        "operationId": "addPageIncidentUpdate",
        "description": "Adds an update to the timeline; the incident takes its status (`resolved` closes it, any other status reopens it). With `notify` (default true) it is emailed to the confirmed subscribers.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          },
          {
            "name": "incident",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the incident."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PageIncidentInput"
              },
              "example": {
                "status": "resolved",
                "message": "Response times are back to normal.",
                "notify": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "incident": {
                      "$ref": "#/components/schemas/PageIncident"
                    },
                    "notified": {
                      "type": "integer",
                      "description": "Emails queued for subscribers."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/status-pages/{id}/incidents/{incident}/updates/{update}": {
      "delete": {
        "tags": [
          "Status pages"
        ],
        "summary": "Delete an incident update",
        "operationId": "deletePageIncidentUpdate",
        "description": "Removes one update; the incident takes the status of the latest one left. The last update cannot be deleted (409): delete the incident instead.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          },
          {
            "name": "incident",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the incident."
          },
          {
            "name": "update",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the update."
          }
        ],
        "responses": {
          "200": {
            "description": "Incident.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PageIncident"
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/status-pages/{id}/subscribers": {
      "get": {
        "tags": [
          "Status pages"
        ],
        "summary": "List the email subscribers of a status page",
        "operationId": "listPageSubscribers",
        "description": "Addresses subscribed to incident updates, confirmed or waiting for confirmation (unconfirmed ones are dropped after 7 days). `enabled` is false when the page has no working email channel.\n\n**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          }
        ],
        "responses": {
          "200": {
            "description": "Subscribers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subscribers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PageSubscriber"
                      }
                    },
                    "enabled": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/status-pages/{id}/subscribers/{subscriber}": {
      "delete": {
        "tags": [
          "Status pages"
        ],
        "summary": "Remove an email subscriber",
        "operationId": "deletePageSubscriber",
        "description": "The address gets no more emails from the page.\n\n**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          },
          {
            "name": "subscriber",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the subscriber."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/oauth/authorize": {
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Start a sign-in",
        "operationId": "oauthAuthorize",
        "description": "Open in a browser (ASWebAuthenticationSession on iOS). The user logs in on the hub if needed and allows the app on a consent page; the browser then goes to `redirect_uri` with `code`, `state` and `iss`, or with `error`.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "response_type",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`code`."
          },
          {
            "name": "client_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`pingtxt-ios` for the iOS app."
          },
          {
            "name": "redirect_uri",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`pingtxt://oauth` for the iOS app."
          },
          {
            "name": "code_challenge",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "BASE64URL(SHA256(code_verifier)), no padding."
          },
          {
            "name": "code_challenge_method",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "`S256`."
          },
          {
            "name": "state",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Random value, checked on return."
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "`read`, `write` or `read write`: write is preselected on the consent page for admins."
          },
          {
            "name": "resource",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional (RFC 8707): `<hub>/api` or `<hub>/mcp`."
          }
        ],
        "responses": {
          "200": {
            "description": "Consent page (HTML)."
          },
          "302": {
            "description": "To the login page, or back to `redirect_uri`."
          },
          "400": {
            "description": "Unknown client or redirect URI not registered (HTML, no redirect)."
          }
        }
      }
    },
    "/oauth/token": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Get or refresh tokens",
        "operationId": "oauthToken",
        "description": "`grant_type=authorization_code` exchanges the code (single use, 5 minutes) for tokens. `grant_type=refresh_token` returns a new access token **and a new refresh token**: the old one stops working, and using it again revokes the whole sign-in, so refresh one request at a time and store the new refresh token before using it.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "grant_type",
                  "client_id"
                ],
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "description": "`authorization_code` or `refresh_token`."
                  },
                  "client_id": {
                    "type": "string",
                    "description": "`pingtxt-ios` for the iOS app."
                  },
                  "code": {
                    "type": "string",
                    "description": "authorization_code: the code from the redirect."
                  },
                  "redirect_uri": {
                    "type": "string",
                    "description": "authorization_code: same as in the authorization request."
                  },
                  "code_verifier": {
                    "type": "string",
                    "description": "authorization_code: the PKCE verifier."
                  },
                  "refresh_token": {
                    "type": "string",
                    "description": "refresh_token: the latest refresh token."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tokens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthTokens"
                }
              }
            }
          },
          "400": {
            "description": "OAuth error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          }
        }
      }
    },
    "/oauth/revoke": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "Sign out",
        "operationId": "oauthRevoke",
        "description": "Ends the sign-in of the token (access or refresh token, RFC 7009). Always answers 200.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "token",
                  "client_id"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "Refresh or access token."
                  },
                  "client_id": {
                    "type": "string",
                    "description": "`pingtxt-ios` for the iOS app."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done (also when the token was unknown)."
          }
        }
      }
    },
    "/.well-known/oauth-authorization-server": {
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "Authorization server metadata",
        "operationId": "oauthServerMetadata",
        "description": "RFC 8414 metadata: endpoints, scopes, PKCE methods.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "responses": {
          "200": {
            "description": "Metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/push": {
      "get": {
        "tags": [
          "Settings"
        ],
        "summary": "Get mobile app push settings",
        "operationId": "getPushSettings",
        "description": "**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Push settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PushSettings"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      },
      "put": {
        "tags": [
          "Settings"
        ],
        "summary": "Save mobile app push settings",
        "operationId": "putPushSettings",
        "description": "Turning push on registers the hub with the relay the first time, and again when the relay no longer accepts the hub token (its secret changed): the phones then enroll again. A different relay URL drops the registration and every phone (they enroll again).\n\n**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "relay_url": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "enabled": true,
                "relay_url": "https://push.pingtxt.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PushSettings"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/settings/push/test": {
      "post": {
        "tags": [
          "Settings"
        ],
        "summary": "Test the push relay",
        "operationId": "testPush",
        "description": "Checks the hub token at the relay and that the relay can reach Firebase.\n\n**Access:** Browser session of an admin only: API tokens get 403.",
        "x-pingtxt-access": "admin",
        "security": [
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "The relay accepts this hub.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "hub_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        }
      }
    },
    "/api/mobile/config": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "Push config for the mobile app",
        "operationId": "getMobileConfig",
        "description": "Where the app enrolls its phone: it sends hub_id and its FCM token to {relay_url}/v1/devices and gives the handle it gets to POST /api/mobile/devices. live_activity is true when the hub also pushes Live Activity updates (PUT /api/mobile/live-activity/...). enabled is false (and the other fields empty) when push is off.\n\n**Access:** Any logged-in user or API token (read scope is enough).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Config.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "relay_url": {
                      "type": "string"
                    },
                    "hub_id": {
                      "type": "string"
                    },
                    "live_activity": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "enabled": true,
                  "relay_url": "https://push.pingtxt.com",
                  "hub_id": "PrBlwRm8tbClvg8f5PEy4w",
                  "live_activity": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/mobile/devices": {
      "get": {
        "tags": [
          "Notifications"
        ],
        "summary": "List my phones",
        "operationId": "listMobileDevices",
        "description": "**Access:** Any logged-in user or token, also read-only users and read-only tokens: phones are personal.",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Phones of the current user.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PushDevice"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Notifications"
        ],
        "summary": "Register my phone",
        "operationId": "saveMobileDevice",
        "description": "Stores the device handle (prd_...) from the push relay for the app's sign-in. Only OAuth access tokens (pma_...) can call it; each sign-in keeps one handle, a new one replaces it. Revoking the sign-in removes the phone.\n\n**Access:** Any logged-in user or token, also read-only users and read-only tokens: phones are personal.",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "device"
                ],
                "properties": {
                  "device": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  }
                }
              },
              "example": {
                "device": "prd_AeYWsf5y...",
                "name": "Federico's iPhone"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "integer",
                      "format": "int64"
                    }
                  }
                },
                "example": {
                  "id": 3
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/mobile/devices/{id}": {
      "delete": {
        "tags": [
          "Notifications"
        ],
        "summary": "Remove my phone",
        "operationId": "deleteMobileDevice",
        "description": "The phone stops getting notifications from this hub.\n\n**Access:** Any logged-in user or token, also read-only users and read-only tokens: phones are personal.",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the phone."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/mobile/live-activity/start-token": {
      "put": {
        "tags": [
          "Notifications"
        ],
        "summary": "Set my Live Activity push-to-start token",
        "operationId": "putLiveActivityStartToken",
        "description": "Stores the ActivityKit push-to-start token of the app (iOS 17.2+, Activity<OutageActivityAttributes>.pushToStartTokenUpdates) and the hub URL the app signed in with. When a monitor goes down and this phone has no activity running, the hub starts one through the push relay with attributes {\"hub\": hub_url}. Send it again whenever iOS gives a new one.\n\n**Access:** Only the pingtxt app's OAuth access tokens (pma_...), also of read-only users: phones are personal. The phone must be registered first with POST /api/mobile/devices (409 otherwise).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token",
                  "hub_url"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "pattern": "^[0-9a-fA-F]{16,1024}$"
                  },
                  "hub_url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              },
              "example": {
                "token": "80f4c3...",
                "hub_url": "https://status.example.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Notifications"
        ],
        "summary": "Remove my Live Activity push-to-start token",
        "operationId": "deleteLiveActivityStartToken",
        "description": "The hub no longer starts the activity on this phone (for example, Live Activities turned off in the app).\n\n**Access:** Only the pingtxt app's OAuth access tokens (pma_...), also of read-only users: phones are personal. The phone must be registered first with POST /api/mobile/devices (409 otherwise).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/mobile/live-activity/token": {
      "put": {
        "tags": [
          "Notifications"
        ],
        "summary": "Set the token of my running Live Activity",
        "operationId": "putLiveActivityToken",
        "description": "Stores the push token of the activity running on this phone (activity.pushTokenUpdates). The hub sends it update and end pushes when incidents open or close, and refreshes it every 15 minutes while incidents stay open. Send it again when it rotates.\n\n**Access:** Only the pingtxt app's OAuth access tokens (pma_...), also of read-only users: phones are personal. The phone must be registered first with POST /api/mobile/devices (409 otherwise).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "pattern": "^[0-9a-fA-F]{16,1024}$"
                  }
                }
              },
              "example": {
                "token": "6a0d1e..."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      },
      "delete": {
        "tags": [
          "Notifications"
        ],
        "summary": "Remove the token of my Live Activity",
        "operationId": "deleteLiveActivityToken",
        "description": "Call it when the activity ends on the phone (ended by the app or dismissed by the user).\n\n**Access:** Only the pingtxt app's OAuth access tokens (pma_...), also of read-only users: phones are personal. The phone must be registered first with POST /api/mobile/devices (409 otherwise).",
        "x-pingtxt-access": "read",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      }
    },
    "/api/status-pages/{id}/logo": {
      "post": {
        "tags": [
          "Status pages"
        ],
        "summary": "Upload a status page logo",
        "operationId": "uploadPageLogo",
        "description": "Raw image file as the body (`Content-Type: application/octet-stream`), up to 4 MB: SVG, PNG, JPEG, GIF, WebP or AVIF. An SVG is kept as a vector, without scripts, event handlers or external references. Other images are trimmed of transparent borders, scaled down to fit 960×192 pixels and stored as AVIF, WebP and PNG. Replaces the current logo.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "logo": {
                      "$ref": "#/components/schemas/PageLogo"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Status pages"
        ],
        "summary": "Remove a status page logo",
        "operationId": "deletePageLogo",
        "description": "**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/status-pages/{id}/logo/url": {
      "post": {
        "tags": [
          "Status pages"
        ],
        "summary": "Import a status page logo from a URL",
        "operationId": "importPageLogo",
        "description": "The hub downloads the image (public http or https addresses only: private and local networks are refused) and stores it like an upload.\n\n**Access:** Admin only: a write-scope API token of an admin, or an admin's browser session. Read-only users and read tokens get 403.",
        "x-pingtxt-access": "write",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "sessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Id of the status page."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "example": "https://example.com/logo.svg"
                  }
                },
                "required": [
                  "url"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "logo": {
                      "$ref": "#/components/schemas/PageLogo"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/status/{slug}/logo/{file}": {
      "get": {
        "tags": [
          "Status pages"
        ],
        "summary": "Status page logo",
        "operationId": "publicPageLogo",
        "description": "One format of the page logo: `logo.svg`, or `logo.avif`, `logo.webp` and `logo.png`. Same access rules as the page (drafts for logged-in users, password protected pages need the unlock cookie). With the current `?v=` the file is cached for a year. On a custom domain the path is `/api/public/status/logo/{file}`.\n\n**Access:** No authentication.",
        "x-pingtxt-access": "public",
        "security": [],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "file",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "logo.svg",
                "logo.avif",
                "logo.webp",
                "logo.png"
              ]
            }
          },
          {
            "name": "v",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Logo version from the page JSON."
          }
        ],
        "responses": {
          "200": {
            "description": "The image.",
            "content": {
              "image/svg+xml": {},
              "image/avif": {},
              "image/webp": {},
              "image/png": {}
            }
          },
          "403": {
            "description": "Password required."
          },
          "404": {
            "description": "No such page or logo."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ptk_... or pma_...",
        "description": "Personal API token from settings → api tokens (ptk_...), or an OAuth access token (pma_...) of a native app or MCP client."
      },
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "pingtxt_session",
        "description": "Browser session, set by login."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "Authorization code with PKCE (S256), public clients only. The iOS app uses client_id `pingtxt-ios` and redirect URI `pingtxt://oauth`.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "/oauth/authorize",
            "tokenUrl": "/oauth/token",
            "refreshUrl": "/oauth/token",
            "scopes": {
              "read": "Read monitors, incidents, results and everything else a read token can.",
              "write": "Also create, change and delete (admins only)."
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid input.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "name is required"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Not logged in, or the token is invalid or expired.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "unauthorized"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Not allowed for this role, token scope or kind of authentication.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "this token is read-only"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "monitor not found"
            }
          }
        }
      },
      "Conflict": {
        "description": "Already exists.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "a group with this name already exists"
            }
          }
        }
      },
      "BadGateway": {
        "description": "The notification could not be delivered.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "slack: 404 Not Found"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Too many failed attempts. Password logins are limited to 10 failures per account and 30 per client address in 15 minutes; second factor codes to 10 wrong codes per user. Wait for `Retry-After` seconds.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until a new attempt is accepted.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "too many failed attempts, try again in 14 minutes"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human readable message.",
            "examples": [
              "monitor not found"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "Ok": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "Always true.",
            "const": true
          }
        },
        "required": [
          "ok"
        ]
      },
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "email": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "description": "admin or viewer (read-only).",
            "enum": [
              "admin",
              "viewer"
            ]
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "has_password": {
            "type": "boolean",
            "description": "false for accounts that only log in with single sign-on."
          },
          "two_factor": {
            "type": "boolean",
            "description": "TOTP two-factor authentication is enabled."
          },
          "identities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Identity"
            }
          }
        }
      },
      "Identity": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "description": "github, google or oidc."
          },
          "email": {
            "type": "string"
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          }
        }
      },
      "APIToken": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "hint": {
            "type": "string",
            "description": "Last 4 characters of the secret."
          },
          "scope": {
            "type": "string",
            "description": "read: GET requests only. write: everything a token can do.",
            "enum": [
              "read",
              "write"
            ]
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "last_used_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "expires_at": {
            "type": "integer",
            "format": "int64",
            "description": "Expiry, 0 = never."
          }
        }
      },
      "Assertion": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "Path in the JSON body: dot notation with [n] for array items, e.g. data.items[0].status.",
            "examples": [
              "status"
            ]
          },
          "op": {
            "type": "string",
            "enum": [
              "eq",
              "ne",
              "contains",
              "not_contains",
              "exists",
              "not_exists",
              "gt",
              "gte",
              "lt",
              "lte",
              "matches"
            ]
          },
          "value": {
            "type": "string",
            "description": "Expected value. Not used by exists / not_exists; a number for gt/gte/lt/lte; a regular expression for matches."
          }
        },
        "required": [
          "path",
          "op"
        ],
        "description": "A rule on the JSON body of an http or keyword response."
      },
      "MonitorInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "http",
              "keyword",
              "tcp",
              "ping",
              "dns",
              "push",
              "postgres",
              "mysql",
              "redis",
              "mongodb",
              "docker",
              "grpc",
              "smtp",
              "mcp"
            ]
          },
          "target": {
            "type": "string",
            "description": "What to check, depends on the type: an http(s) URL (http, keyword, mcp), host:port (tcp, IPv6 in brackets: [2001:db8::1]:443), a hostname or IP (ping, dns), a connection URL (postgres://, mysql://, redis://, mongodb://), a container name or id (docker), grpc[s]://host:port[/service], smtp[s]://host[:port]. Empty for push.",
            "examples": [
              "https://example.com/health"
            ]
          },
          "interval_sec": {
            "type": "integer",
            "description": "Seconds between checks, 10 to 86400 (push: expected period, up to 7 days). 0 = default (60, push 3600)."
          },
          "timeout_ms": {
            "type": "integer",
            "description": "500 to 60000. 0 = 10000."
          },
          "retries": {
            "type": "integer",
            "description": "Consecutive failures before a location counts as down, 1 to 10."
          },
          "method": {
            "type": "string",
            "description": "HTTP method for http/keyword (default GET). For mcp: the transport, auto, streamable or sse."
          },
          "expected_status": {
            "type": "string",
            "description": "Accepted HTTP status codes, e.g. \"200-299\" or \"200,301\". For mcp: the minimum number of tools."
          },
          "keyword": {
            "type": "string",
            "description": "keyword: text that must appear in the body. mcp: comma separated tool names that must be listed."
          },
          "keyword_invert": {
            "type": "boolean",
            "description": "keyword: fail when the text is found."
          },
          "dns_record": {
            "type": "string",
            "description": "dns: record type (A, AAAA, CNAME, MX, TXT, NS...)."
          },
          "dns_server": {
            "type": "string",
            "description": "dns: resolver to query, host[:port]. Empty = system resolver."
          },
          "dns_expected": {
            "type": "string",
            "description": "dns: value that must be in the answer."
          },
          "ignore_tls": {
            "type": "boolean",
            "description": "Accept invalid TLS certificates."
          },
          "ip_family": {
            "type": "integer",
            "enum": [
              0,
              1,
              4,
              6,
              46
            ],
            "description": "0 = instance default (GeneralSettings.ip_family), 1 = IPv4 or IPv6 (whichever connects first), 4 = IPv4 only, 6 = IPv6 only, 46 = IPv4 and IPv6 (both checked, the check fails when either fails). Locations without IPv6 check over IPv4 with 0 and 1; 6 and 46 are refused on them (see Location.ipv6). Ignored for push and docker."
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Request headers (http, keyword, mcp)."
          },
          "body": {
            "type": "string",
            "description": "Request body (http, keyword)."
          },
          "assertions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Assertion"
            },
            "description": "JSON response rules, http and keyword only, at most 20."
          },
          "grace_sec": {
            "type": "integer",
            "description": "push: extra time after the period before the monitor goes down, 0 to 86400."
          },
          "degraded_pct": {
            "type": "integer",
            "description": "Share of failing locations (percent) from which the monitor is degraded, 1 to 50. 0 = instance default."
          },
          "locations": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Where the check runs: \"local\" (the hub) and/or \"agent:<id>\". Empty = local. See GET /api/locations.",
            "examples": [
              [
                "local",
                "agent:2"
              ]
            ]
          },
          "group_id": {
            "type": "integer",
            "format": "int64",
            "description": "Group, 0 = none."
          },
          "tag_ids": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            }
          },
          "notifiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChannelLink"
            },
            "description": "Notification channels linked to this monitor one by one (notifier_id + events). Channels whose scope covers the monitor need no link. Left out on update: the links are kept; [] removes them."
          },
          "cert_expiry_alert": {
            "type": "boolean",
            "description": "Alert before the TLS certificate expires (https targets)."
          },
          "domain_expiry_alert": {
            "type": "boolean",
            "description": "Alert before the domain registration expires."
          },
          "paused": {
            "type": "boolean",
            "description": "Create the monitor paused. Ignored on update: use POST /api/monitors/{id}/pause."
          }
        },
        "required": [
          "name",
          "type"
        ]
      },
      "Monitor": {
        "allOf": [
          {
            "$ref": "#/components/schemas/MonitorInput"
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "format": "int64"
              },
              "push_token": {
                "type": "string",
                "description": "push: token of the heartbeat URL. Hidden from read-only users."
              },
              "status": {
                "type": "string",
                "enum": [
                  "up",
                  "degraded",
                  "down",
                  "pending",
                  "paused",
                  "maintenance"
                ]
              },
              "status_since": {
                "type": "integer",
                "format": "int64",
                "description": "Unix time in milliseconds (0 = never / unknown)."
              },
              "created_at": {
                "type": "integer",
                "format": "int64",
                "description": "Unix time in milliseconds (0 = never / unknown)."
              }
            }
          }
        ]
      },
      "Result": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "monitor_id": {
            "type": "integer",
            "format": "int64"
          },
          "location": {
            "type": "string",
            "description": "local, agent:<id> or push."
          },
          "ts": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "ok": {
            "type": "boolean"
          },
          "latency_ms": {
            "type": "number"
          },
          "status_code": {
            "type": "integer"
          },
          "message": {
            "type": "string",
            "description": "Error or detail of the check."
          },
          "cert_expiry": {
            "type": "integer",
            "format": "int64",
            "description": "TLS certificate expiry seen by this check."
          },
          "maint": {
            "type": "boolean",
            "description": "Taken during a maintenance window."
          },
          "timing": {
            "$ref": "#/components/schemas/Phases",
            "description": "HTTP checks only, from the hub and agents that measure phases."
          }
        }
      },
      "Bar": {
        "type": "object",
        "properties": {
          "t": {
            "type": "integer",
            "format": "int64",
            "description": "Start of the half hour."
          },
          "total": {
            "type": "integer",
            "description": "Checks outside maintenance."
          },
          "ok": {
            "type": "integer",
            "description": "Checks outside maintenance taken while the monitor was up or degraded. One failing location among several does not lower it."
          },
          "degraded": {
            "type": "integer",
            "description": "Checks outside maintenance taken while the monitor was degraded."
          },
          "maint": {
            "type": "integer",
            "description": "Checks during maintenance."
          },
          "avg_ms": {
            "type": "number"
          }
        }
      },
      "ExpiryCheck": {
        "type": "object",
        "properties": {
          "monitor_id": {
            "type": "integer",
            "format": "int64"
          },
          "kind": {
            "type": "string",
            "enum": [
              "cert",
              "domain"
            ]
          },
          "subject": {
            "type": "string",
            "description": "Host name (cert) or registrable domain."
          },
          "expires_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "issuer": {
            "type": "string",
            "description": "Certificate issuer or registrar."
          },
          "source": {
            "type": "string",
            "enum": [
              "tls",
              "rdap",
              "whois"
            ]
          },
          "error": {
            "type": "string",
            "description": "Last lookup error, if any."
          },
          "checked_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          }
        }
      },
      "MonitorSummary": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Monitor"
          },
          {
            "type": "object",
            "properties": {
              "uptime_24h": {
                "type": "number",
                "description": "Share of checks taken while the monitor was up or degraded (not of single location checks), maintenance excluded. -1 without data."
              },
              "avg_ms_24h": {
                "type": "number"
              },
              "last": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/Result"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "bars": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Bar"
                },
                "description": "Last 24 hours in 48 half-hour bars."
              },
              "expiry": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ExpiryCheck"
                }
              },
              "maintenance": {
                "oneOf": [
                  {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer",
                        "format": "int64"
                      },
                      "name": {
                        "type": "string"
                      },
                      "end_at": {
                        "type": "integer",
                        "format": "int64",
                        "description": "Unix time in milliseconds (0 = never / unknown)."
                      }
                    }
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Window covering the monitor right now."
              }
            }
          }
        ]
      },
      "MonitorDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/MonitorSummary"
          },
          {
            "type": "object",
            "properties": {
              "uptime_7d": {
                "type": "number",
                "description": "Share of checks taken while the monitor was up or degraded (not of single location checks), maintenance excluded. -1 without data."
              },
              "uptime_30d": {
                "type": "number",
                "description": "Share of checks taken while the monitor was up or degraded (not of single location checks), maintenance excluded. -1 without data."
              },
              "location_status": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "online": {
                      "type": "boolean"
                    },
                    "last": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Result"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              },
              "push_url": {
                "type": "string",
                "description": "push: full heartbeat URL."
              }
            }
          }
        ]
      },
      "Bucket": {
        "type": "object",
        "properties": {
          "t": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "location": {
            "type": "string"
          },
          "total": {
            "type": "integer"
          },
          "ok": {
            "type": "integer",
            "description": "Per location: checks that succeeded there. Otherwise: checks taken while the monitor was up or degraded."
          },
          "degraded": {
            "type": "integer",
            "description": "Checks taken while the monitor was degraded (0 per location)."
          },
          "maint": {
            "type": "integer"
          },
          "avg_ms": {
            "type": "number"
          },
          "min_ms": {
            "type": "number"
          },
          "max_ms": {
            "type": "number"
          },
          "phases": {
            "$ref": "#/components/schemas/Phases",
            "description": "Average per phase of the OK HTTP checks that measured it."
          }
        }
      },
      "Incident": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "monitor_id": {
            "type": "integer",
            "format": "int64"
          },
          "monitor_name": {
            "type": "string"
          },
          "started_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "resolved_at": {
            "type": "integer",
            "format": "int64",
            "description": "0 while open."
          },
          "cause": {
            "type": "string"
          }
        }
      },
      "Group": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string",
            "description": "Unique, up to 64 characters."
          },
          "description": {
            "type": "string",
            "description": "Up to 500 characters."
          },
          "monitors": {
            "type": "integer",
            "description": "Number of monitors in the group (read only)."
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          }
        },
        "required": [
          "name"
        ]
      },
      "Tag": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string",
            "description": "Unique, up to 32 characters, no commas."
          },
          "color": {
            "type": "string",
            "enum": [
              "",
              "red",
              "orange",
              "yellow",
              "green",
              "cyan",
              "blue",
              "purple",
              "pink",
              "gray"
            ]
          },
          "monitors": {
            "type": "integer",
            "description": "Number of tagged monitors (read only)."
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          }
        },
        "required": [
          "name"
        ]
      },
      "MaintenanceInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "all_monitors": {
            "type": "boolean",
            "description": "Covers every monitor."
          },
          "monitor_ids": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            }
          },
          "group_ids": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Every monitor of these groups."
          },
          "tag_ids": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Every monitor with one of these tags."
          },
          "kind": {
            "type": "string",
            "enum": [
              "once",
              "daily",
              "weekly",
              "monthly"
            ]
          },
          "start_at": {
            "type": "integer",
            "format": "int64",
            "description": "once: start. Recurring: first day the schedule is valid (0 = now)."
          },
          "end_at": {
            "type": "integer",
            "format": "int64",
            "description": "once: end. Recurring: last day the schedule is valid (0 = forever)."
          },
          "start_time": {
            "type": "string",
            "description": "Recurring: local start time, HH:MM.",
            "examples": [
              "02:00"
            ]
          },
          "duration_min": {
            "type": "integer",
            "description": "Recurring: length in minutes, up to 7 days."
          },
          "weekdays": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "weekly: 0 = Sunday … 6 = Saturday."
          },
          "month_days": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "monthly: 1 … 31."
          },
          "timezone": {
            "type": "string",
            "description": "IANA time zone, default UTC.",
            "examples": [
              "Europe/Rome"
            ]
          },
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "name",
          "kind"
        ]
      },
      "Maintenance": {
        "allOf": [
          {
            "$ref": "#/components/schemas/MaintenanceInput"
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "format": "int64"
              },
              "created_at": {
                "type": "integer",
                "format": "int64",
                "description": "Unix time in milliseconds (0 = never / unknown)."
              },
              "active": {
                "type": "boolean",
                "description": "Running now."
              },
              "active_end": {
                "type": "integer",
                "format": "int64",
                "description": "Unix time in milliseconds (0 = never / unknown)."
              },
              "next_start": {
                "type": "integer",
                "format": "int64",
                "description": "Next occurrence."
              },
              "next_end": {
                "type": "integer",
                "format": "int64",
                "description": "Unix time in milliseconds (0 = never / unknown)."
              }
            }
          }
        ]
      },
      "Agent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "location": {
            "type": "string",
            "description": "Free text, e.g. \"Frankfurt\"."
          },
          "version": {
            "type": "string"
          },
          "hostname": {
            "type": "string"
          },
          "platform": {
            "type": "string"
          },
          "remote_addr": {
            "type": "string"
          },
          "last_seen": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "ipv6": {
            "type": "integer",
            "enum": [
              0,
              1,
              2
            ],
            "description": "IPv6 connectivity as last probed: 0 unknown (agents before this field), 1 available, 2 not available. IPv6-only monitors cannot use a location with 2, and monitors with ip_family 0 check over IPv4 there."
          },
          "location_id": {
            "type": "string",
            "description": "Value to use in monitor locations.",
            "examples": [
              "agent:2"
            ]
          },
          "online": {
            "type": "boolean"
          },
          "monitors": {
            "type": "integer",
            "description": "Number of monitors that check from this agent."
          },
          "update_available": {
            "type": "boolean"
          },
          "self_update": {
            "type": "boolean",
            "description": "The agent can replace its own binary."
          },
          "no_update_reason": {
            "type": "string"
          },
          "update_state": {
            "type": "string",
            "enum": [
              "updating",
              "failed"
            ]
          },
          "update_error": {
            "type": "string"
          }
        }
      },
      "Location": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "online": {
            "type": "boolean"
          },
          "kind": {
            "type": "string",
            "enum": [
              "local",
              "agent"
            ]
          },
          "ipv6": {
            "type": "integer",
            "enum": [
              0,
              1,
              2
            ],
            "description": "IPv6 connectivity as last probed: 0 unknown (agents before this field), 1 available, 2 not available. IPv6-only monitors cannot use a location with 2, and monitors with ip_family 0 check over IPv4 there."
          }
        }
      },
      "AgentMonitor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "target": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "enum": [
              "no_ipv6",
              "ipv6_unknown",
              "docker",
              "loopback"
            ],
            "description": "Why the monitor is skipped: no_ipv6 = the monitor checks over IPv6 only (or IPv4 and IPv6) and the agent has no IPv6; ipv6_unknown = same, but the agent has not reported IPv6 yet; docker = Docker checks look at a container on the probe's own host; loopback = the target is localhost."
          }
        }
      },
      "Notifier": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "webhook",
              "discord",
              "slack",
              "telegram",
              "ntfy",
              "email",
              "app"
            ]
          },
          "config": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "webhook, discord, slack, ntfy: {url}. telegram: {bot_token, chat_id}. email: {host, port, security (starttls|tls|none), username, password, from, to}. app: {users}, emails of the users whose phones get the alerts, comma separated (empty = every user). The email password is write-only: responses carry password_set=\"true\" instead, and an empty password on update keeps the stored one. Read-only users see secrets as ********."
          },
          "enabled": {
            "type": "boolean"
          },
          "delay_min": {
            "type": "integer",
            "minimum": 0,
            "maximum": 10080,
            "description": "Minutes a monitor must stay down before this channel gets its first alert (0 = right away). Use it for an escalation contact: shorter outages never reach it, and it gets the recovery message only if it was alerted."
          },
          "repeat_min": {
            "type": "integer",
            "minimum": 0,
            "maximum": 10080,
            "description": "Repeat the alert every this many minutes while the monitor stays down (0 = once, otherwise at least 5). Reminders carry reminder=1, 2, ... and down_since in the webhook payload. Nothing is sent during maintenance windows."
          },
          "all_monitors": {
            "type": "boolean",
            "description": "The channel covers every monitor, also the ones added later. Default true when left out on create."
          },
          "group_ids": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Without all_monitors: the channel covers the monitors of these groups."
          },
          "tag_ids": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Without all_monitors: the channel covers the monitors with one of these tags."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "down",
                "up",
                "degraded",
                "cert_expiry",
                "domain_expiry"
              ]
            },
            "description": "What the channel gets for the monitors it covers. Default on create: down, up, cert_expiry, domain_expiry. down = the monitor went down (and the reminders of repeat_min), up = it is back up (only to channels that got the down alert), degraded = some locations fail and when that ends, cert_expiry / domain_expiry = the TLS certificate or the domain registration expires soon, expired or was renewed."
          },
          "monitors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChannelLink"
            },
            "description": "Monitors linked one by one (monitor_id + events), the same links as the monitors' notifiers. Left out on update: kept; [] removes them."
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          }
        },
        "required": [
          "name",
          "type",
          "config"
        ]
      },
      "StatusPage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "slug": {
            "type": "string",
            "description": "URL path: /status/<slug>. 1-50 lowercase letters, digits or dashes."
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "domain": {
            "type": "string",
            "description": "Optional custom domain serving the page, e.g. status.example.com."
          },
          "header_url": {
            "type": "string",
            "description": "Optional http(s) address the page name links to, e.g. the company website. Empty = the name is not a link."
          },
          "hide_title": {
            "type": "boolean",
            "description": "Show only the logo in the page header, without the page name (it stays the browser tab title). Ignored when the page has no logo."
          },
          "logo": {
            "$ref": "#/components/schemas/PageLogo",
            "readOnly": true,
            "description": "The page logo, only in the answer of GET /api/status-pages/{id}. Change it with the logo routes."
          },
          "published": {
            "type": "boolean",
            "description": "Drafts are visible only to logged-in users."
          },
          "show_incidents": {
            "type": "boolean"
          },
          "monitor_ids": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            }
          },
          "group_ids": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Every monitor of these groups, current and future."
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "subscribe_notifier_id": {
            "type": "integer",
            "format": "int64",
            "description": "Email notification channel that sends incident updates to the page's subscribers (its SMTP server and sender are used, its recipients are not). 0 = visitors cannot subscribe."
          },
          "protected": {
            "type": "boolean",
            "description": "Visitors need a password to open the page, its badges and its subscription form. Logged-in users open it without one. On save, false removes the password and true keeps the current one."
          },
          "password": {
            "type": "string",
            "writeOnly": true,
            "description": "New password for visitors (6-72 characters), stored hashed and never returned. Needed when protected is set on a page without one."
          }
        },
        "required": [
          "slug",
          "title"
        ]
      },
      "PublicStatusPage": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "header_url": {
            "type": "string",
            "description": "Address the page name links to, empty = no link."
          },
          "logo": {
            "$ref": "#/components/schemas/PageLogo",
            "description": "Present when the page has a logo."
          },
          "hide_title": {
            "type": "boolean",
            "description": "Show only the logo in the header."
          },
          "published": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "operational",
              "degraded",
              "partial_outage",
              "major_outage",
              "unknown"
            ],
            "description": "Overall state, from the monitors and the open incidents written on the page (minor impact = degraded, major = partial outage, critical = major outage), whichever is worse."
          },
          "days": {
            "type": "integer",
            "description": "Number of daily bars."
          },
          "monitors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "format": "int64"
                },
                "name": {
                  "type": "string"
                },
                "group": {
                  "type": "string",
                  "description": "Section title, empty = no section."
                },
                "status": {
                  "type": "string"
                },
                "uptime": {
                  "type": "number",
                  "description": "Over the shown days. Share of checks taken while the monitor was up or degraded (not of single location checks), maintenance excluded. -1 without data."
                },
                "uptime_24h": {
                  "type": "number",
                  "description": "Share of checks taken while the monitor was up or degraded (not of single location checks), maintenance excluded. -1 without data."
                },
                "bars": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Bar"
                  },
                  "description": "One bar per day."
                }
              }
            }
          },
          "show_incidents": {
            "type": "boolean"
          },
          "incidents": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "format": "int64"
                },
                "monitor_name": {
                  "type": "string"
                },
                "started_at": {
                  "type": "integer",
                  "format": "int64",
                  "description": "Unix time in milliseconds (0 = never / unknown)."
                },
                "resolved_at": {
                  "type": "integer",
                  "format": "int64",
                  "description": "Unix time in milliseconds (0 = never / unknown)."
                }
              }
            }
          },
          "updated_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "reports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PageIncident"
            },
            "description": "Incidents written on the page: the open ones and those resolved in the shown days, open first."
          },
          "subscribe": {
            "type": "boolean",
            "description": "Visitors can subscribe to incident updates by email."
          }
        }
      },
      "GeneralSettings": {
        "type": "object",
        "properties": {
          "public_url": {
            "type": "string",
            "description": "Public URL of the hub, used in links, agent setup and OAuth callbacks."
          },
          "local_name": {
            "type": "string",
            "description": "Name of the hub's own location."
          },
          "local_enabled": {
            "type": "boolean",
            "description": "The hub runs checks itself."
          },
          "retention_days": {
            "type": "integer",
            "description": "Hourly history, up to 3650."
          },
          "raw_retention_days": {
            "type": "integer",
            "description": "Every single check."
          },
          "monitor_defaults": {
            "type": "object",
            "properties": {
              "interval_sec": {
                "type": "integer"
              },
              "timeout_ms": {
                "type": "integer"
              },
              "retries": {
                "type": "integer"
              }
            }
          },
          "expiry": {
            "type": "object",
            "properties": {
              "cert_days": {
                "type": "array",
                "items": {
                  "type": "integer"
                },
                "description": "Alert thresholds in days."
              },
              "domain_days": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              }
            }
          },
          "metrics": {
            "type": "object",
            "properties": {
              "public": {
                "type": "boolean",
                "description": "Serve /metrics without a token."
              }
            }
          },
          "degraded_pct": {
            "type": "integer",
            "description": "Default degraded threshold, 1 to 50."
          },
          "ip_family": {
            "type": "integer",
            "enum": [
              1,
              4,
              6,
              46
            ],
            "description": "Default IP family of monitors with ip_family 0: 1 IPv4 or IPv6 (default), 4 IPv4 only, 6 IPv6 only, 46 IPv4 and IPv6. Locations without IPv6 check those monitors over IPv4."
          },
          "update_check": {
            "type": "object",
            "properties": {
              "disabled": {
                "type": "boolean",
                "description": "Stop asking GitHub for new releases. PINGTXT_DISABLE_UPDATE_CHECK=true sets it on first start."
              }
            }
          }
        }
      },
      "AuthSettings": {
        "type": "object",
        "properties": {
          "password_login": {
            "type": "boolean"
          },
          "allow_signup": {
            "type": "boolean",
            "description": "Create accounts on first single sign-on login."
          },
          "allowed_domains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Email domains allowed to sign up, empty = any."
          },
          "default_role": {
            "type": "string",
            "enum": [
              "admin",
              "viewer"
            ]
          },
          "providers": {
            "type": "object",
            "description": "Keyed by github, google, oidc.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "enabled": {
                  "type": "boolean"
                },
                "client_id": {
                  "type": "string"
                },
                "client_secret": {
                  "type": "string",
                  "description": "Write-only: empty keeps the stored one."
                },
                "name": {
                  "type": "string",
                  "description": "oidc: button label."
                },
                "issuer": {
                  "type": "string",
                  "description": "oidc: issuer URL."
                },
                "scopes": {
                  "type": "string",
                  "description": "oidc: extra scopes."
                }
              }
            }
          }
        }
      },
      "Settings": {
        "type": "object",
        "properties": {
          "general": {
            "$ref": "#/components/schemas/GeneralSettings"
          },
          "auth": {
            "type": "object",
            "properties": {
              "password_login": {
                "type": "boolean"
              },
              "allow_signup": {
                "type": "boolean"
              },
              "allowed_domains": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "default_role": {
                "type": "string"
              },
              "providers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "label": {
                      "type": "string"
                    },
                    "enabled": {
                      "type": "boolean"
                    },
                    "client_id": {
                      "type": "string"
                    },
                    "client_secret": {
                      "type": "string",
                      "description": "Always empty."
                    },
                    "client_secret_set": {
                      "type": "boolean"
                    },
                    "callback_url": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "issuer": {
                      "type": "string"
                    },
                    "scopes": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "detected_url": {
            "type": "string",
            "description": "URL of the hub as seen in this request."
          }
        }
      },
      "BackupSettings": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "interval_hours": {
            "type": "integer"
          },
          "keep": {
            "type": "integer",
            "description": "Scheduled backups to keep."
          }
        }
      },
      "BackupFile": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "auto",
              "manual",
              "upload",
              "pre-restore",
              "pre-import"
            ]
          },
          "size": {
            "type": "integer",
            "format": "int64"
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          }
        }
      },
      "BackupInfo": {
        "type": "object",
        "properties": {
          "schema_version": {
            "type": "integer"
          },
          "monitors": {
            "type": "integer"
          },
          "users": {
            "type": "integer"
          }
        }
      },
      "PendingRestore": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "staged_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          }
        }
      },
      "ConfigDocument": {
        "type": "object",
        "properties": {
          "format": {
            "type": "string",
            "description": "Always \"pingtxt-config\"."
          },
          "version": {
            "type": "integer"
          },
          "hub_version": {
            "type": "string"
          },
          "exported_at": {
            "type": "string"
          },
          "secrets": {
            "type": "boolean",
            "description": "Passwords and tokens are included."
          },
          "settings": {
            "type": "object"
          },
          "agents": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "groups": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "notifiers": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "monitors": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "maintenances": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "status_pages": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        },
        "description": "Monitors, agents, notification channels, groups, tags, maintenance windows, status pages and settings, referenced by name. Export one with GET /api/config/export to see the full shape."
      },
      "ImportCounts": {
        "type": "object",
        "additionalProperties": {
          "type": "integer"
        },
        "description": "Counts per kind (monitors, agents, ...)."
      },
      "ImportResult": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "merge",
              "replace"
            ]
          },
          "backup": {
            "type": "string",
            "description": "Backup taken before the import."
          },
          "created": {
            "$ref": "#/components/schemas/ImportCounts"
          },
          "updated": {
            "$ref": "#/components/schemas/ImportCounts"
          },
          "deleted": {
            "$ref": "#/components/schemas/ImportCounts"
          },
          "skipped": {
            "$ref": "#/components/schemas/ImportCounts"
          },
          "settings": {
            "type": "boolean",
            "description": "Instance settings were applied."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "OAuthGrant": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "client_id": {
            "type": "string"
          },
          "client_name": {
            "type": "string"
          },
          "scope": {
            "type": "string"
          },
          "expires_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "last_used_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "push_device": {
            "type": "string",
            "description": "Name of the phone that gets push notifications through this sign-in, if any."
          }
        }
      },
      "VersionStatus": {
        "type": "object",
        "properties": {
          "current": {
            "type": "string",
            "description": "Version of this hub, e.g. v0.1.0 (dev or main-abc1234 for development and branch builds)."
          },
          "check_enabled": {
            "type": "boolean",
            "description": "The hub looks for new releases: on in the settings and running a release build."
          },
          "check_disabled": {
            "type": "boolean",
            "description": "Turned off in the settings."
          },
          "latest": {
            "type": [
              "object",
              "null"
            ],
            "description": "Latest published release, null until the first check.",
            "properties": {
              "version": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "description": "Release notes on GitHub."
              },
              "published_at": {
                "type": "integer",
                "description": "Unix ms."
              }
            }
          },
          "update_available": {
            "type": "boolean",
            "description": "latest is newer than current."
          },
          "checked_at": {
            "type": "integer",
            "description": "Unix ms of the last check, 0 if none yet."
          },
          "error": {
            "type": "string",
            "description": "Why the last check failed."
          }
        }
      },
      "TelemetryStatus": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "asked": {
            "type": "boolean",
            "description": "An admin answered the opt-in question."
          },
          "forced_off": {
            "type": "boolean",
            "description": "PINGTXT_DISABLE_TELEMETRY is set or this is a development build."
          },
          "report": {
            "type": [
              "object",
              "null"
            ],
            "description": "Exactly what the next hourly report sends (null when off).",
            "properties": {
              "id": {
                "type": "string",
                "description": "Random instance id, never changes."
              },
              "version": {
                "type": "string"
              },
              "monitors": {
                "type": "integer",
                "description": "Monitors not paused."
              },
              "agents": {
                "type": "integer",
                "description": "Agents connected now."
              }
            }
          },
          "sent_at": {
            "type": "integer",
            "description": "Last report sent (unix ms), 0 if none yet."
          },
          "error": {
            "type": "string",
            "description": "Last send error."
          }
        }
      },
      "ReportSettings": {
        "type": "object",
        "properties": {
          "weekly": {
            "type": "boolean",
            "description": "Send a report every Monday for the week before."
          },
          "monthly": {
            "type": "boolean",
            "description": "Send a report on the 1st for the month before."
          },
          "hour": {
            "type": "integer",
            "minimum": 0,
            "maximum": 23,
            "description": "Hour of the day the reports are sent, in `timezone`."
          },
          "timezone": {
            "type": "string",
            "description": "IANA time zone of the periods and of the times in the report.",
            "examples": [
              "Europe/Rome"
            ]
          },
          "notifier_ids": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Email channels that receive the reports. Disabled channels are skipped."
          },
          "group_ids": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Only these monitor groups. Empty: every monitor."
          }
        }
      },
      "Reports": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ReportSettings"
          },
          {
            "type": "object",
            "properties": {
              "next": {
                "type": "object",
                "additionalProperties": {
                  "type": "integer",
                  "format": "int64"
                },
                "description": "When the next report of each enabled kind goes out (unix ms), by kind."
              },
              "last": {
                "type": "object",
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "from": {
                      "type": "integer",
                      "format": "int64",
                      "description": "Start of the period (unix ms)."
                    },
                    "sent_at": {
                      "type": "integer",
                      "format": "int64",
                      "description": "Unix ms."
                    }
                  }
                },
                "description": "Last report sent, by kind."
              }
            }
          }
        ]
      },
      "ReportIncident": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "monitor_id": {
            "type": "integer",
            "format": "int64"
          },
          "monitor": {
            "type": "string"
          },
          "started_at": {
            "type": "integer",
            "format": "int64"
          },
          "resolved_at": {
            "type": "integer",
            "format": "int64",
            "description": "0 while open."
          },
          "duration_ms": {
            "type": "integer",
            "format": "int64",
            "description": "Part of the incident inside the period."
          },
          "cause": {
            "type": "string"
          }
        }
      },
      "ReportMonitor": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "uptime": {
            "type": "number",
            "description": "0 to 1, -1 without data. Degraded counts as up, maintenance is left out."
          },
          "prev_uptime": {
            "type": "number",
            "description": "0 to 1, -1 without data. Degraded counts as up, maintenance is left out."
          },
          "avg_ms": {
            "type": "number"
          },
          "incidents": {
            "type": "integer"
          },
          "downtime_ms": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "ReportGroup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "description": "0: monitors without a group."
          },
          "name": {
            "type": "string"
          },
          "uptime": {
            "type": "number",
            "description": "Average uptime of the monitors of the group, -1 without data."
          },
          "prev_uptime": {
            "type": "number"
          },
          "incidents": {
            "type": "integer"
          },
          "downtime_ms": {
            "type": "integer",
            "format": "int64"
          },
          "monitors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportMonitor"
            }
          },
          "incident_list": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportIncident"
            }
          }
        }
      },
      "Report": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "weekly",
              "monthly"
            ]
          },
          "from": {
            "type": "integer",
            "format": "int64",
            "description": "Start of the period (unix ms, included)."
          },
          "to": {
            "type": "integer",
            "format": "int64",
            "description": "End of the period (unix ms, excluded)."
          },
          "timezone": {
            "type": "string"
          },
          "uptime": {
            "type": "number",
            "description": "Average uptime of the monitors, -1 without data."
          },
          "prev_uptime": {
            "type": "number",
            "description": "Same for the period before."
          },
          "incidents": {
            "type": "integer"
          },
          "downtime_ms": {
            "type": "integer",
            "format": "int64"
          },
          "monitors": {
            "type": "integer"
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReportGroup"
            }
          },
          "subject": {
            "type": "string",
            "description": "Email subject."
          },
          "text": {
            "type": "string",
            "description": "Plain text body of the email."
          },
          "url": {
            "type": "string",
            "description": "Public URL of the hub, when set."
          }
        }
      },
      "Phases": {
        "type": "object",
        "description": "Time per phase of an HTTP check, in ms. With redirects the phases of every hop are added up; what is left of the latency is reading the response body.",
        "properties": {
          "dns_ms": {
            "type": "number"
          },
          "connect_ms": {
            "type": "number"
          },
          "tls_ms": {
            "type": "number"
          },
          "ttfb_ms": {
            "type": "number",
            "description": "Wait from the request being sent to the first byte of the response."
          },
          "total_ms": {
            "type": "number",
            "description": "Buckets only: average latency of the checks the phases come from."
          },
          "n": {
            "type": "integer",
            "description": "Buckets only: how many checks the averages come from."
          }
        }
      },
      "PageIncidentUpdate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "incident_id": {
            "type": "integer",
            "format": "int64"
          },
          "status": {
            "type": "string",
            "enum": [
              "investigating",
              "identified",
              "monitoring",
              "resolved"
            ]
          },
          "message": {
            "type": "string"
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          }
        }
      },
      "PageIncident": {
        "type": "object",
        "description": "An incident written on a status page, with its timeline of updates.",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "page_id": {
            "type": "integer",
            "format": "int64"
          },
          "title": {
            "type": "string"
          },
          "impact": {
            "type": "string",
            "enum": [
              "none",
              "minor",
              "major",
              "critical"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "investigating",
              "identified",
              "monitoring",
              "resolved"
            ],
            "description": "Status of the latest update."
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "updated_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "resolved_at": {
            "type": "integer",
            "format": "int64",
            "description": "When it was resolved, unix ms. 0 = still open."
          },
          "updates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PageIncidentUpdate"
            },
            "description": "Newest first."
          }
        }
      },
      "PageIncidentInput": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "One line, at most 200 characters. Required to create and to edit."
          },
          "impact": {
            "type": "string",
            "enum": [
              "none",
              "minor",
              "major",
              "critical"
            ],
            "description": "Default minor."
          },
          "status": {
            "type": "string",
            "enum": [
              "investigating",
              "identified",
              "monitoring",
              "resolved"
            ],
            "description": "Status of the update. Default investigating when creating."
          },
          "message": {
            "type": "string",
            "description": "Text of the update, at most 5000 characters. Required to create and for updates."
          },
          "notify": {
            "type": "boolean",
            "description": "Email the update to the confirmed subscribers of the page (default true). Ignored when editing the title or impact."
          }
        }
      },
      "PageSubscriber": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "page_id": {
            "type": "integer",
            "format": "int64"
          },
          "email": {
            "type": "string"
          },
          "confirmed_at": {
            "type": "integer",
            "format": "int64",
            "description": "When the address confirmed, unix ms. 0 = waiting for confirmation."
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          }
        }
      },
      "OAuthError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "invalid_request, invalid_grant, unsupported_grant_type..."
          },
          "error_description": {
            "type": "string"
          }
        }
      },
      "OAuthTokens": {
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Bearer token for the API and /mcp (pma_...)."
          },
          "token_type": {
            "type": "string",
            "enum": [
              "Bearer"
            ]
          },
          "expires_in": {
            "type": "integer",
            "description": "Seconds the access token lasts (3600)."
          },
          "refresh_token": {
            "type": "string",
            "description": "Single use (pmr_...): every refresh returns a new one."
          },
          "scope": {
            "type": "string",
            "enum": [
              "read",
              "write"
            ]
          }
        }
      },
      "PushSettings": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Alerts can be sent to the mobile app."
          },
          "relay_url": {
            "type": "string",
            "description": "Base URL of the push relay.",
            "default": "https://push.pingtxt.com"
          },
          "hub_id": {
            "type": "string",
            "description": "Id of this hub at the relay (public: the app sends it to the relay)."
          },
          "registered": {
            "type": "boolean",
            "description": "The hub has a token from the relay. The token itself is never returned."
          },
          "devices": {
            "type": "integer",
            "description": "Phones registered by all users."
          }
        }
      },
      "PushDevice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          },
          "last_seen_at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix time in milliseconds (0 = never / unknown)."
          }
        }
      },
      "TestResult": {
        "type": "object",
        "properties": {
          "location": {
            "type": "string",
            "description": "Location id: local or agent:<id>."
          },
          "name": {
            "type": "string",
            "description": "Location name."
          },
          "ok": {
            "type": "boolean"
          },
          "skipped": {
            "type": "boolean",
            "description": "The location could not run the check (offline or turned off); message says why."
          },
          "latency_ms": {
            "type": "number"
          },
          "status_code": {
            "type": "integer"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "location",
          "name",
          "ok",
          "latency_ms"
        ]
      },
      "PageLogo": {
        "type": "object",
        "description": "Logo of a status page. The files are served next to the page JSON: `/api/public/status/{slug}/logo/logo.svg` for a vector logo, else `logo.avif`, `logo.webp` and `logo.png`, each with `?v={v}` to be cached for long.",
        "properties": {
          "svg": {
            "type": "boolean",
            "description": "The logo is an SVG (cleaned of scripts and external references); otherwise it is an image served as AVIF, WebP and PNG."
          },
          "width": {
            "type": "integer",
            "description": "Pixel width of the images, or the SVG viewBox width (0 = unknown)."
          },
          "height": {
            "type": "integer",
            "description": "Pixel height of the images, or the SVG viewBox height (0 = unknown)."
          },
          "v": {
            "type": "integer",
            "format": "int64",
            "description": "Version of the logo (when it was last changed, Unix ms)."
          }
        }
      },
      "ChannelLink": {
        "type": "object",
        "description": "A notification channel linked to one monitor with its own events. It wins over the channel's scope: it adds the monitor, changes its events, or with no events leaves the monitor out of a channel that covers it.",
        "properties": {
          "monitor_id": {
            "type": "integer",
            "format": "int64",
            "description": "In a channel's monitors list."
          },
          "notifier_id": {
            "type": "integer",
            "format": "int64",
            "description": "In a monitor's notifiers list."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "down",
                "up",
                "degraded",
                "cert_expiry",
                "domain_expiry"
              ]
            },
            "description": "down = the monitor went down (and the reminders of repeat_min), up = it is back up (only to channels that got the down alert), degraded = some locations fail and when that ends, cert_expiry / domain_expiry = the TLS certificate or the domain registration expires soon, expired or was renewed."
          }
        },
        "required": [
          "events"
        ]
      }
    }
  }
}
