{
  "openapi": "3.1.0",
  "info": {
    "title": "Pingtower API",
    "version": "v1",
    "description": "Generated from the daemon's route tables — internal/server's Mux() and internal/hosted's Register() — so this document cannot describe a contract the hosted service does not keep."
  },
  "servers": [
    {
      "url": "https://api.pingtower.com",
      "description": "The hosted Pingtower API."
    }
  ],
  "tags": [
    {
      "name": "actions",
      "description": "Operator-defined HTTP runbook actions and their execution log."
    },
    {
      "name": "alerts",
      "description": "The alert stream, and acknowledging or resolving what it carries."
    },
    {
      "name": "devices",
      "description": "Push-notification device registration, scoped to the calling account."
    },
    {
      "name": "ingest",
      "description": "Writing events into a project from an instrumented service."
    },
    {
      "name": "integrations",
      "description": "Typed delivery channels — webhook, Telegram, Slack, and APNs."
    },
    {
      "name": "logs",
      "description": "Retained log lines, for the sources configured to keep them."
    },
    {
      "name": "oncall",
      "description": "On-call schedules, their overrides, and who is paging right now."
    },
    {
      "name": "pairing",
      "description": "Linking a hosted account to a box account, and deciding whose phone rings."
    },
    {
      "name": "projects",
      "description": "Projects — the namespace every source, rule, and report lives in."
    },
    {
      "name": "relay",
      "description": "Receiving a self-hosted box's alerts and paging its tenant's phones."
    },
    {
      "name": "reports",
      "description": "Incident reports and the shareable public status page behind each one."
    },
    {
      "name": "rules",
      "description": "Match conditions that turn events into alerts, and their escalation ladders."
    },
    {
      "name": "sources",
      "description": "Named event producers within a project, each holding an ingest token."
    },
    {
      "name": "webhooks",
      "description": "The plain delivery-endpoint surface over the shared outbox."
    }
  ],
  "paths": {
    "/v1/account/devices": {
      "get": {
        "operationId": "listAccountDevices",
        "summary": "List this account's device push tokens (account plane)",
        "description": "Every device token registered to the calling session's account, with the platform and last-seen time of each. The session-authenticated counterpart to GET /v1/devices.",
        "tags": [
          "devices"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "devices": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "critical_alerts": {
                        "type": "boolean"
                      },
                      "last_seen_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "platform": {
                        "type": "string"
                      },
                      "token": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "required": [
                "devices"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "sessionBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/account/devices \\\n  -H \"Authorization: Bearer pts_...\""
      },
      "post": {
        "operationId": "registerAccountDevice",
        "summary": "Register a device push token (account plane)",
        "description": "The same registry as POST /v1/devices, reached with a session token instead of a tenant API key. Prefer this one.\n\nRegistering a handset was never a tenant-scoped act — a device token belongs to an account, and the tenant key was only ever expressing which bearer the client happened to hold. Authenticating it with a tenant key becomes wrong once a tenant's data plane can live on a box the tenant runs: that tenant's only data key is minted by its box, and no data key issued anywhere would reach this registry. A session token has neither problem, and it exists before any tenant is selected.\n\nBody, token format, critical_alerts, and the 20-per-account limit are identical to POST /v1/devices. The tenant-key routes remain for shipped clients.",
        "tags": [
          "devices"
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "critical_alerts": {
                "type": "boolean"
              },
              "platform": {
                "type": "string"
              },
              "token": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "registered": {
                  "type": "boolean"
                }
              },
              "required": [
                "registered"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "422": {
            "description": "The request is well-formed but references something that cannot be used.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "sessionBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/account/devices \\\n  -H \"Authorization: Bearer pts_...\" \\\n  -d '{\"token\":\"<64 hex>\",\"platform\":\"ios\",\"critical_alerts\":true}'"
      }
    },
    "/v1/account/devices/{token}": {
      "delete": {
        "operationId": "unregisterAccountDevice",
        "summary": "Unregister a device push token (account plane)",
        "description": "The sign-out half of the contract above, session-authenticated. Call it before a different account signs into the same physical device, or the outgoing account's row lingers and keeps pushing to that handset.",
        "tags": [
          "devices"
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The 64-character hex device token, as registered.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Success, with no response body."
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "sessionBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/account/devices/<64 hex> \\\n  -H \"Authorization: Bearer pts_...\""
      }
    },
    "/v1/alerts/{id}/ack": {
      "post": {
        "operationId": "ack",
        "summary": "Acknowledge an alert",
        "description": "Marks the alert as being worked on, which stops its renotify timer without closing it. Acknowledging an alert that is already resolved is a conflict, not a no-op.",
        "tags": [
          "alerts"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The alert id, as returned by GET /v1/pull.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "count": {
                  "type": "integer"
                },
                "escalation_level": {
                  "type": "integer"
                },
                "first_seen": {
                  "type": "integer"
                },
                "id": {
                  "type": "string"
                },
                "identifiers": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                },
                "investigate": {
                  "type": "boolean"
                },
                "last_message": {
                  "type": "string"
                },
                "last_seen": {
                  "type": "integer"
                },
                "level": {
                  "type": "integer"
                },
                "page_accounts": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "project": {
                  "type": "string"
                },
                "rule": {
                  "type": "string"
                },
                "state": {
                  "type": "string"
                },
                "template": {
                  "type": "string"
                },
                "updated_seq": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/alerts/42/ack \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/alerts/{id}/resolve": {
      "post": {
        "operationId": "resolve",
        "summary": "Resolve an alert",
        "description": "Closes the alert. A resolved alert stops counting against the project's open_alerts and stops renotifying; the matching rule opens a new alert if the condition recurs.",
        "tags": [
          "alerts"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The alert id, as returned by GET /v1/pull.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "count": {
                  "type": "integer"
                },
                "escalation_level": {
                  "type": "integer"
                },
                "first_seen": {
                  "type": "integer"
                },
                "id": {
                  "type": "string"
                },
                "identifiers": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                },
                "investigate": {
                  "type": "boolean"
                },
                "last_message": {
                  "type": "string"
                },
                "last_seen": {
                  "type": "integer"
                },
                "level": {
                  "type": "integer"
                },
                "page_accounts": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "project": {
                  "type": "string"
                },
                "rule": {
                  "type": "string"
                },
                "state": {
                  "type": "string"
                },
                "template": {
                  "type": "string"
                },
                "updated_seq": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/alerts/42/resolve \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/devices": {
      "get": {
        "operationId": "listDevices",
        "summary": "List the account's registered devices",
        "description": "Every device token registered to the calling key's account — not to its tenant. An account in three tenants sees one list, whichever tenant's key it asks with.",
        "tags": [
          "devices"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "devices": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "critical_alerts": {
                        "type": "boolean"
                      },
                      "last_seen_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "platform": {
                        "type": "string"
                      },
                      "token": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "required": [
                "devices"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/devices \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "post": {
        "operationId": "registerDevice",
        "summary": "Register a device push token",
        "description": "The token is account-scoped, not tenant-scoped: the same handset registered from two tenants resolves to one row per account. Another account registering the same physical device does not retire this account's row.\n\nThat makes DELETE /v1/devices/{token} the client's sign-out obligation. Call it before a different account signs in on this device, or the outgoing account keeps receiving its tenant's pushes here. An uninstall is reclaimed without client cooperation — APNs reports the token permanently gone and the daemon prunes it.\n\nThe token must be exactly 64 hex characters, the format Apple's token-based provider API expects. The strictness is deliberate: two strings differing only in case or in stray characters would otherwise each take their own row while addressing the same handset.\n\ncritical_alerts is optional and defaults to false. It is the handset's own answer to whether a critical push would actually break through Do Not Disturb, a Focus, or a silenced ringer — an omitted key (every shipped App Store build today) means the daemon must not treat this device as able to receive one. Every registration is a complete, fresh report: re-registering with a changed value overwrites what is stored, in either direction.",
        "tags": [
          "devices"
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "critical_alerts": {
                "type": "boolean"
              },
              "platform": {
                "type": "string"
              },
              "token": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "registered": {
                  "type": "boolean"
                }
              },
              "required": [
                "registered"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "422": {
            "description": "The request is well-formed but references something that cannot be used.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/devices \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"token\":\"<64 hex>\",\"platform\":\"ios\",\"critical_alerts\":true}'"
      }
    },
    "/v1/devices/{token}": {
      "delete": {
        "operationId": "unregisterDevice",
        "summary": "Unregister a device push token",
        "description": "The sign-out half of the contract above. Call this before a different account signs into the same physical device, or the outgoing account's row lingers and keeps pushing to that handset.",
        "tags": [
          "devices"
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The 64-character hex device token, as registered.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Success, with no response body."
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/devices/<64 hex> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/ingest": {
      "post": {
        "operationId": "ingest",
        "summary": "Ingest one event",
        "description": "The only route an ingest token may call. The token's tenant segment picks the store; the secret is verified against that tenant's own sources table, and the source it resolves to decides which project the event lands in — nothing in the body names a project.\n\nLevels are HTTP-ish: 100 debug, 200 info, 300 warn, 400 error, 500 critical. Key values arrive as arbitrary JSON scalars and are stringified. The per-field caps are defence in depth below the overall body limit: one event is a log line plus a handful of structured keys, not a bulk payload.",
        "tags": [
          "ingest"
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "keys": {
                "type": "object",
                "maxProperties": 128,
                "additionalProperties": {}
              },
              "level": {
                "type": "integer",
                "minimum": 0
              },
              "message": {
                "type": "string",
                "minLength": 0,
                "maxLength": 65536
              },
              "tags": {
                "type": "array",
                "maxItems": 64,
                "items": {
                  "type": "string"
                }
              }
            },
            "required": [
              "message"
            ]
          }
        },
        "responses": {
          "202": {
            "description": "Status 202.",
            "schema": {
              "type": "object",
              "properties": {
                "alerted": {
                  "type": "boolean"
                },
                "template_id": {
                  "type": "string"
                }
              },
              "required": [
                "alerted",
                "template_id"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "ingestBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/ingest \\\n  -H \"Authorization: Bearer pti_<tenant>_...\" \\\n  -d '{\"message\":\"user bob logged in\",\"level\":200,\n       \"keys\":{\"name\":\"bob\"},\"tags\":[\"auth\"]}'"
      }
    },
    "/v1/pair": {
      "post": {
        "operationId": "pair",
        "summary": "Redeem a pairing code for a data credential",
        "description": "The box half of pairing, and the only route in the product that mints a tenant API key without a session. The code is the credential: a one-shot secret the box's operator issued with `pingtower pair` and read out to a person, valid for an hour and unrecoverable from the database afterwards.\n\nWhat comes back is a `ptk_` this box minted for an account this box holds, so the data plane it opens is the box's own — no assertion signed elsewhere, and nothing that stops working when pingtower.com does. The receipt alongside it is for pingtower.com, which redeems it once at GET /v1/pair/verify to satisfy itself that the person claiming this pairing really did pair.\n\nEvery failure answers 401 with one message. \"Already redeemed\" and \"no such code\" are exactly the distinction a holder of a candidate code would like back, and neither helps the legitimate holder, who has the operator standing next to them.",
        "tags": [
          "pairing"
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "minLength": 48,
                "maxLength": 48
              },
              "display_name": {
                "type": "string",
                "maxLength": 128
              },
              "email": {
                "type": "string",
                "format": "email",
                "maxLength": 320
              },
              "password": {
                "type": "string",
                "minLength": 8,
                "maxLength": 72
              }
            },
            "required": [
              "code"
            ]
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "api_key": {
                  "type": "string"
                },
                "api_key_id": {
                  "type": "string"
                },
                "box_account_id": {
                  "type": "string"
                },
                "receipt": {
                  "type": "string"
                },
                "tenant_id": {
                  "type": "string"
                }
              },
              "required": [
                "api_key",
                "api_key_id",
                "box_account_id",
                "receipt",
                "tenant_id"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "403": {
            "description": "The key's membership role does not permit this operation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [],
        "x-pingtower-example": "curl -X POST https://box.example.com/v1/pair \\\n  -d '{\"code\":\"<48 hex>\",\"display_name\":\"nightowl\",\n       \"email\":\"bob@corp.com\",\"password\":\"<8-72 bytes>\"}'"
      }
    },
    "/v1/pair/codes": {
      "post": {
        "operationId": "createPairingCode",
        "summary": "Issue a pairing code for someone joining this box",
        "description": "Mints a one-shot code that admits one person to this tenant, and returns a join code carrying it once this box knows its counterpart on pingtower.com. Manager-gated: issuing a code is the act of admitting somebody, which is the same rank that decides who this tenant's pages reach.\n\nIt exists so that adding a teammate does not require a shell on the box. `pingtower pair` is still the bootstrap path and still the one that works when nothing else does — on a new box there is no account, no membership, and no key to authenticate this call with.\n\nThe code names a **seat**, not an account: whoever redeems it brings or creates their own account on this box, with a password they choose. Nothing is created here, so a code that is never redeemed leaves nothing behind — unlike the CLI's account-bound form, which creates an account, a membership, and a generated password whether or not anybody turns up.\n\n`email` optionally binds the seat to one address, and is worth setting whenever the issuer knows who they are inviting: an unbound seat is one that whoever holds the code may take, so a leaked code admits a stranger rather than the intended person. A code may not be issued at a role above the issuer's own.\n\n`ttl_seconds` defaults to an hour and is capped at seven days. An hour suits a code read out while somebody is standing there; a seat sent by chat is acted on later.",
        "tags": [
          "pairing"
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string",
                "format": "email",
                "maxLength": 320
              },
              "role": {
                "type": "string",
                "enum": [
                  "owner",
                  "admin",
                  "member"
                ]
              },
              "ttl_seconds": {
                "type": "integer",
                "minimum": 60,
                "maximum": 604800
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created."
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "403": {
            "description": "The key's membership role does not permit this operation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-role": "manager",
        "x-pingtower-example": "curl -X POST https://box.example.com/v1/pair/codes \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"role\":\"member\",\"email\":\"bob@corp.com\",\"ttl_seconds\":86400}'",
        "x-pingtower-unresolved": [
          "response:201"
        ]
      }
    },
    "/v1/pair/verify": {
      "get": {
        "operationId": "pairVerify",
        "summary": "Redeem a pairing receipt (called by pingtower.com)",
        "description": "The other end of the callback. pingtower.com fetches this once, while the user is waiting, to learn which of the box's accounts a receipt vouches for — without it, any signed-in account that learned a tenant id could attach itself to that tenant's roster.\n\nIt answers the box account id and nothing else, and the receipt is one-shot and short-lived, so a caller who somehow guessed one learns an identifier they had already claimed to know.\n\nClients do not call this. It is documented because a self-hoster's box must answer it, and because an operator reading their access log is entitled to know what pingtower.com asked for.",
        "tags": [
          "pairing"
        ],
        "parameters": [
          {
            "name": "receipt",
            "in": "query",
            "required": false,
            "description": "The one-shot receipt POST /v1/pair returned.",
            "schema": {
              "type": "string",
              "maxLength": 48
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "box_account_id": {
                  "type": "string"
                },
                "tenant_id": {
                  "type": "string"
                }
              },
              "required": [
                "box_account_id",
                "tenant_id"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [],
        "x-pingtower-example": "curl \"https://box.example.com/v1/pair/verify?receipt=<48 hex>\""
      }
    },
    "/v1/projects": {
      "get": {
        "operationId": "listProjects",
        "summary": "List the tenant's projects",
        "description": "The project index: every project the API key's tenant owns, each entry carrying its name, config version, source and rule counts, open alert count, and creation time, name-ordered. Paginate with after, which takes the name the previous page ended on.",
        "tags": [
          "projects"
        ],
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Resume after this project name.",
            "schema": {
              "type": "string",
              "maxLength": 200
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum projects to return.",
            "schema": {
              "type": "integer",
              "default": 200,
              "minimum": 1,
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success."
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl \"https://api.pingtower.com/v1/projects?limit=50\" \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\"",
        "x-pingtower-unresolved": [
          "response:200"
        ]
      },
      "post": {
        "operationId": "createProject",
        "summary": "Create a project",
        "description": "Projects are explicit rows rather than namespaces conjured on first write, so a typo in a later request fails with a 404 instead of silently creating a second project. The name is caller-chosen and is the id every nested route addresses.",
        "tags": [
          "projects"
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 128
              }
            },
            "required": [
              "name"
            ]
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "schema": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "version": {
                  "type": "integer"
                }
              },
              "required": [
                "name",
                "version"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"name\":\"demo\"}'"
      }
    },
    "/v1/projects/{id}": {
      "delete": {
        "operationId": "deleteProject",
        "summary": "Delete a project",
        "description": "Removes the project with its sources, rules, alerts, and reports. Any public status pages the project's reports published stop resolving. Requires an owner or admin key.",
        "tags": [
          "projects"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deleted": {
                  "type": "boolean"
                }
              },
              "required": [
                "deleted"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "403": {
            "description": "The key's membership role does not permit this operation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-role": "manager",
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/projects/demo \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/actions": {
      "get": {
        "operationId": "listActions",
        "summary": "List runbook actions",
        "description": "Every action on the project. Stored headers and bodies marshal as \"-\" rather than in cleartext.",
        "tags": [
          "actions"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "created_at": {
                        "type": "integer"
                      },
                      "enabled": {
                        "type": "boolean"
                      },
                      "event_filter": {
                        "type": "string"
                      },
                      "has_body": {
                        "type": "boolean"
                      },
                      "header_keys": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "id": {
                        "type": "string"
                      },
                      "method": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      },
                      "project": {
                        "type": "string"
                      },
                      "timeout_seconds": {
                        "type": "integer"
                      },
                      "url": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "required": [
                "actions"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/actions \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "post": {
        "operationId": "createAction",
        "summary": "Create a runbook action",
        "description": "An action is an HTTP call the daemon makes on your behalf when an alert fires. events defaults to alert.opened; alert.escalated is opt-in per action. The timeout is capped so a slow target cannot pin the worker, and header names and values are length- and charset-limited like every other field here.\n\nheaders and body are sealed at rest and never echoed back by a read, so a read-modify-write update that omits them keeps what is stored rather than clearing it.",
        "tags": [
          "actions"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "body": {
                "type": "string",
                "maxLength": 8192
              },
              "enabled": {
                "type": "boolean"
              },
              "events": {
                "type": "array",
                "maxItems": 2,
                "items": {
                  "type": "string",
                  "enum": [
                    "alert.opened",
                    "alert.escalated"
                  ]
                }
              },
              "headers": {
                "type": "object",
                "maxProperties": 32,
                "additionalProperties": {
                  "type": "string"
                }
              },
              "method": {
                "type": "string",
                "enum": [
                  "GET",
                  "POST",
                  "PUT",
                  "PATCH",
                  "DELETE"
                ]
              },
              "name": {
                "type": "string",
                "maxLength": 128
              },
              "timeout_seconds": {
                "type": "integer",
                "minimum": 0,
                "maximum": 60
              },
              "url": {
                "type": "string",
                "format": "uri",
                "maxLength": 2048
              }
            },
            "required": [
              "method",
              "name",
              "url"
            ]
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "schema": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                }
              },
              "required": [
                "id"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "403": {
            "description": "The key's membership role does not permit this operation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/actions \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"name\":\"restart-api\",\"method\":\"POST\",\n       \"url\":\"https://ops.internal/restart\",\"timeout_seconds\":10}'"
      }
    },
    "/v1/projects/{id}/actions/{aid}": {
      "get": {
        "operationId": "getAction",
        "summary": "Read one runbook action",
        "description": "The action as stored, with its sealed headers and body redacted.",
        "tags": [
          "actions"
        ],
        "parameters": [
          {
            "name": "aid",
            "in": "path",
            "required": true,
            "description": "The action id.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "created_at": {
                  "type": "integer"
                },
                "enabled": {
                  "type": "boolean"
                },
                "event_filter": {
                  "type": "string"
                },
                "has_body": {
                  "type": "boolean"
                },
                "header_keys": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "id": {
                  "type": "string"
                },
                "method": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "project": {
                  "type": "string"
                },
                "timeout_seconds": {
                  "type": "integer"
                },
                "url": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/actions/<aid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "put": {
        "operationId": "updateAction",
        "summary": "Update a runbook action",
        "description": "An omitted enabled, headers, or body keeps the stored value rather than resetting it — necessary because a read never hands the sealed fields back, so a client that reads then writes cannot resend them. Send body as an explicit empty string to clear it.",
        "tags": [
          "actions"
        ],
        "parameters": [
          {
            "name": "aid",
            "in": "path",
            "required": true,
            "description": "The action id.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "body": {
                "type": "string",
                "maxLength": 8192
              },
              "enabled": {
                "type": "boolean"
              },
              "events": {
                "type": "array",
                "maxItems": 2,
                "items": {
                  "type": "string",
                  "enum": [
                    "alert.opened",
                    "alert.escalated"
                  ]
                }
              },
              "headers": {
                "type": "object",
                "maxProperties": 32,
                "additionalProperties": {
                  "type": "string"
                }
              },
              "method": {
                "type": "string",
                "enum": [
                  "GET",
                  "POST",
                  "PUT",
                  "PATCH",
                  "DELETE"
                ]
              },
              "name": {
                "type": "string",
                "maxLength": 128
              },
              "timeout_seconds": {
                "type": "integer",
                "minimum": 0,
                "maximum": 60
              },
              "url": {
                "type": "string",
                "format": "uri",
                "maxLength": 2048
              }
            },
            "required": [
              "method",
              "name",
              "url"
            ]
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "updated": {
                  "type": "boolean"
                }
              },
              "required": [
                "updated"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X PUT https://api.pingtower.com/v1/projects/demo/actions/<aid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"name\":\"restart-api\",\"method\":\"POST\",\n       \"url\":\"https://ops.internal/restart\",\"enabled\":false}'"
      },
      "delete": {
        "operationId": "deleteAction",
        "summary": "Delete a runbook action",
        "description": "Removes the action. Its execution-log rows stay.",
        "tags": [
          "actions"
        ],
        "parameters": [
          {
            "name": "aid",
            "in": "path",
            "required": true,
            "description": "The action id.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deleted": {
                  "type": "boolean"
                }
              },
              "required": [
                "deleted"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/projects/demo/actions/<aid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/actions/{aid}/executions": {
      "get": {
        "operationId": "listActionExecutions",
        "summary": "List an action's executions",
        "description": "The execution log the background drain worker fills in as alert-triggered runs settle, plus any terminal rows from test fires.",
        "tags": [
          "actions"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum execution rows to return.",
            "schema": {
              "type": "integer",
              "default": 100,
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "name": "aid",
            "in": "path",
            "required": true,
            "description": "The action id.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "executions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "action_id": {
                        "type": "string"
                      },
                      "alert_id": {
                        "type": "string"
                      },
                      "attempts": {
                        "type": "integer"
                      },
                      "created_at": {
                        "type": "integer"
                      },
                      "duration_ms": {
                        "type": "integer"
                      },
                      "error": {
                        "type": "string"
                      },
                      "event": {
                        "type": "string"
                      },
                      "executed_at": {
                        "type": "integer"
                      },
                      "id": {
                        "type": "string"
                      },
                      "response_prefix": {
                        "type": "string"
                      },
                      "status": {
                        "type": "string"
                      },
                      "status_code": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "required": [
                "executions"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl \"https://api.pingtower.com/v1/projects/demo/actions/<aid>/executions?limit=50\" \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/actions/{aid}/test": {
      "post": {
        "operationId": "testAction",
        "summary": "Test-fire a runbook action",
        "description": "Runs the action once, synchronously, with its stored headers and body, through the same guarded executor and per-action timeout every alert-driven run uses. The result is recorded as one terminal execution-log row rather than a retryable queued one.\n\nA daemon started without a configured executor answers 503.",
        "tags": [
          "actions"
        ],
        "parameters": [
          {
            "name": "aid",
            "in": "path",
            "required": true,
            "description": "The action id.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "ok": {
                  "type": "boolean"
                },
                "status_code": {
                  "type": "integer"
                }
              },
              "required": [
                "ok",
                "status_code"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "503": {
            "description": "Status 503.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/actions/<aid>/test \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/integrations": {
      "get": {
        "operationId": "listIntegrations",
        "summary": "List typed integrations",
        "description": "Every channel on the project. Credentials are never echoed back. An apns row additionally carries device_count, joined at request time from the account's registered devices.",
        "tags": [
          "integrations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "integrations": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "consecutive_failures": {
                        "type": "integer"
                      },
                      "created_at": {
                        "type": "integer"
                      },
                      "device_count": {
                        "type": "integer"
                      },
                      "enabled": {
                        "type": "boolean"
                      },
                      "event_filter": {
                        "type": "string"
                      },
                      "id": {
                        "type": "string"
                      },
                      "last_error": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      },
                      "project": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      },
                      "url": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "required": [
                "integrations"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/integrations \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "post": {
        "operationId": "createIntegration",
        "summary": "Create a typed integration",
        "description": "Which credential fields are required depends on type: webhook needs url (secret is optional and minted when absent), telegram needs token and chat_id, slack needs webhook_url.\n\nAn apns integration takes no credentials at all. The push provider key is one operator-wide credential rather than a per-project one, and its recipients come from tenant membership — so every credential field is rejected on that type rather than quietly ignored.",
        "tags": [
          "integrations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "chat_id": {
                "type": "string"
              },
              "events": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "name": {
                "type": "string"
              },
              "secret": {
                "type": "string"
              },
              "token": {
                "type": "string"
              },
              "type": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "webhook_url": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created."
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "422": {
            "description": "The request is well-formed but references something that cannot be used.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/integrations \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"type\":\"slack\",\"name\":\"eng-alerts\",\n       \"webhook_url\":\"https://hooks.slack.com/services/...\"}'",
        "x-pingtower-unresolved": [
          "response:201"
        ]
      }
    },
    "/v1/projects/{id}/integrations/{iid}": {
      "get": {
        "operationId": "getIntegration",
        "summary": "Read one integration",
        "description": "The channel as stored, minus its credentials.",
        "tags": [
          "integrations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "iid",
            "in": "path",
            "required": true,
            "description": "The integration id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "consecutive_failures": {
                  "type": "integer"
                },
                "created_at": {
                  "type": "integer"
                },
                "device_count": {
                  "type": "integer"
                },
                "enabled": {
                  "type": "boolean"
                },
                "event_filter": {
                  "type": "string"
                },
                "id": {
                  "type": "string"
                },
                "last_error": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "project": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/integrations/<iid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "put": {
        "operationId": "updateIntegration",
        "summary": "Update an integration",
        "description": "type is pinned server-side to the stored value and cannot be changed. A blank credential field keeps what is already stored, and an omitted name or events key keeps what is already stored — only a key that is present, including an explicitly empty one, changes anything. Read a channel, edit one field, send it back: nothing you did not mention moves.",
        "tags": [
          "integrations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "iid",
            "in": "path",
            "required": true,
            "description": "The integration id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "chat_id": {
                "type": "string"
              },
              "events": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "name": {
                "type": "string"
              },
              "secret": {
                "type": "string"
              },
              "token": {
                "type": "string"
              },
              "type": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "webhook_url": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "updated": {
                  "type": "boolean"
                }
              },
              "required": [
                "updated"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "422": {
            "description": "The request is well-formed but references something that cannot be used.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X PUT https://api.pingtower.com/v1/projects/demo/integrations/<iid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"name\":\"eng-alerts-oncall\"}'"
      },
      "delete": {
        "operationId": "deleteIntegration",
        "summary": "Delete an integration",
        "description": "Removes the channel and its stored credentials.",
        "tags": [
          "integrations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "iid",
            "in": "path",
            "required": true,
            "description": "The integration id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deleted": {
                  "type": "boolean"
                }
              },
              "required": [
                "deleted"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/projects/demo/integrations/<iid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/integrations/{iid}/deliveries": {
      "get": {
        "operationId": "listIntegrationDeliveries",
        "summary": "List an integration's deliveries",
        "description": "The delivery log for one channel — what was attempted, when, and how it ended. This is where a silently failing channel becomes visible.",
        "tags": [
          "integrations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "iid",
            "in": "path",
            "required": true,
            "description": "The integration id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deliveries": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "alert_id": {
                        "type": "string"
                      },
                      "attempts": {
                        "type": "integer"
                      },
                      "created_at": {
                        "type": "integer"
                      },
                      "event": {
                        "type": "string"
                      },
                      "id": {
                        "type": "string"
                      },
                      "last_error": {
                        "type": "string"
                      },
                      "next_attempt_at": {
                        "type": "integer"
                      },
                      "status": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "required": [
                "deliveries"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl \"https://api.pingtower.com/v1/projects/demo/integrations/<iid>/deliveries?limit=50\" \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/integrations/{iid}/disable": {
      "post": {
        "operationId": "disableIntegration",
        "summary": "Disable an integration",
        "description": "Stops delivery through the channel while keeping its configuration and credentials in place.",
        "tags": [
          "integrations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "iid",
            "in": "path",
            "required": true,
            "description": "The integration id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "enabled": {
                  "type": "boolean"
                }
              },
              "required": [
                "enabled"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/integrations/<iid>/disable \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/integrations/{iid}/enable": {
      "post": {
        "operationId": "enableIntegration",
        "summary": "Enable an integration",
        "description": "Resumes delivery through the channel and clears its failure streak — the way back from a circuit-broken endpoint.",
        "tags": [
          "integrations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "iid",
            "in": "path",
            "required": true,
            "description": "The integration id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "enabled": {
                  "type": "boolean"
                }
              },
              "required": [
                "enabled"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/integrations/<iid>/enable \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/integrations/{iid}/test": {
      "post": {
        "operationId": "testIntegration",
        "summary": "Test-fire an integration",
        "description": "Sends a synthetic alert.test event through the channel synchronously, so you learn whether the credentials work without waiting on the drain tick or opening a real alert. The response reports the channel's own answer, so a 200 here can still carry ok=false with the remote error.\n\nA daemon started without a configured sender answers 503: delivery is optional, and the rest of the API runs without it.",
        "tags": [
          "integrations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "iid",
            "in": "path",
            "required": true,
            "description": "The integration id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "ok": {
                  "type": "boolean"
                }
              },
              "required": [
                "ok"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "503": {
            "description": "Status 503.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/integrations/<iid>/test \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/logtail": {
      "get": {
        "operationId": "logTail",
        "summary": "Read retained log lines",
        "description": "Returns the most recent retained events for the project, newest last. Only sources created or updated with retain_logs set have anything here; the rest keep no line-level history at all.",
        "tags": [
          "logs"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum lines to return.",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "template",
            "in": "query",
            "required": false,
            "description": "Filter to one extracted message template.",
            "schema": {
              "type": "string",
              "maxLength": 128
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "messages": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "required": [
                "messages"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl \"https://api.pingtower.com/v1/projects/demo/logtail?limit=50\" \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/oncall/schedules": {
      "get": {
        "operationId": "listOnCallSchedules",
        "summary": "List on-call schedules",
        "description": "Every schedule on the project, with its layers and participants.",
        "tags": [
          "oncall"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "schedules": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "created_at": {
                        "type": "integer"
                      },
                      "id": {
                        "type": "string"
                      },
                      "layers": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "participants": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            "restriction": {
                              "type": "object",
                              "properties": {
                                "days": {
                                  "type": "array",
                                  "items": {
                                    "type": "integer"
                                  }
                                },
                                "end_minute": {
                                  "type": "integer"
                                },
                                "start_minute": {
                                  "type": "integer"
                                }
                              }
                            },
                            "rotation_start": {
                              "type": "integer"
                            },
                            "shift_minutes": {
                              "type": "integer"
                            }
                          }
                        }
                      },
                      "name": {
                        "type": "string"
                      },
                      "timezone": {
                        "type": "string"
                      },
                      "updated_at": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "required": [
                "schedules"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/oncall/schedules \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "post": {
        "operationId": "createOnCallSchedule",
        "summary": "Create an on-call schedule",
        "description": "Every participant is checked against the tenant's memberships at write time. A schedule routing through a non-member would silently page nobody, so it is rejected with a 422 listing each offending account rather than accepted and left to fail quietly at 3am.",
        "tags": [
          "oncall"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "layers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "participants": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "restriction": {
                      "type": "object",
                      "properties": {
                        "days": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "end_minute": {
                          "type": "integer"
                        },
                        "start_minute": {
                          "type": "integer"
                        }
                      }
                    },
                    "rotation_start": {
                      "type": "integer"
                    },
                    "shift_minutes": {
                      "type": "integer"
                    }
                  }
                }
              },
              "name": {
                "type": "string"
              },
              "timezone": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "schema": {
              "type": "object",
              "properties": {
                "created_at": {
                  "type": "integer"
                },
                "id": {
                  "type": "string"
                },
                "layers": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "participants": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "restriction": {
                        "type": "object",
                        "properties": {
                          "days": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            }
                          },
                          "end_minute": {
                            "type": "integer"
                          },
                          "start_minute": {
                            "type": "integer"
                          }
                        }
                      },
                      "rotation_start": {
                        "type": "integer"
                      },
                      "shift_minutes": {
                        "type": "integer"
                      }
                    }
                  }
                },
                "name": {
                  "type": "string"
                },
                "timezone": {
                  "type": "string"
                },
                "updated_at": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "422": {
            "description": "The request is well-formed but references something that cannot be used.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/oncall/schedules \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"name\":\"primary\",\"timezone\":\"UTC\",\n       \"layers\":[{\"rotation\":\"weekly\",\"participants\":[\"<account_id>\"]}]}'"
      }
    },
    "/v1/projects/{id}/oncall/schedules/{sid}": {
      "get": {
        "operationId": "getOnCallSchedule",
        "summary": "Read one on-call schedule",
        "description": "The schedule as stored, including its layers.",
        "tags": [
          "oncall"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "description": "The schedule name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "created_at": {
                  "type": "integer"
                },
                "id": {
                  "type": "string"
                },
                "layers": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "participants": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "restriction": {
                        "type": "object",
                        "properties": {
                          "days": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            }
                          },
                          "end_minute": {
                            "type": "integer"
                          },
                          "start_minute": {
                            "type": "integer"
                          }
                        }
                      },
                      "rotation_start": {
                        "type": "integer"
                      },
                      "shift_minutes": {
                        "type": "integer"
                      }
                    }
                  }
                },
                "name": {
                  "type": "string"
                },
                "timezone": {
                  "type": "string"
                },
                "updated_at": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "put": {
        "operationId": "updateOnCallSchedule",
        "summary": "Update an on-call schedule",
        "description": "Replaces the schedule's layers and participants, re-checking every participant against the tenant's memberships.",
        "tags": [
          "oncall"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "description": "The schedule name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "layers": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "participants": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "restriction": {
                      "type": "object",
                      "properties": {
                        "days": {
                          "type": "array",
                          "items": {
                            "type": "integer"
                          }
                        },
                        "end_minute": {
                          "type": "integer"
                        },
                        "start_minute": {
                          "type": "integer"
                        }
                      }
                    },
                    "rotation_start": {
                      "type": "integer"
                    },
                    "shift_minutes": {
                      "type": "integer"
                    }
                  }
                }
              },
              "name": {
                "type": "string"
              },
              "timezone": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "created_at": {
                  "type": "integer"
                },
                "id": {
                  "type": "string"
                },
                "layers": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "participants": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "restriction": {
                        "type": "object",
                        "properties": {
                          "days": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            }
                          },
                          "end_minute": {
                            "type": "integer"
                          },
                          "start_minute": {
                            "type": "integer"
                          }
                        }
                      },
                      "rotation_start": {
                        "type": "integer"
                      },
                      "shift_minutes": {
                        "type": "integer"
                      }
                    }
                  }
                },
                "name": {
                  "type": "string"
                },
                "timezone": {
                  "type": "string"
                },
                "updated_at": {
                  "type": "integer"
                }
              }
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "422": {
            "description": "The request is well-formed but references something that cannot be used.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X PUT https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"name\":\"primary\",\"timezone\":\"UTC\",\n       \"layers\":[{\"rotation\":\"daily\",\"participants\":[\"<account_id>\"]}]}'"
      },
      "delete": {
        "operationId": "deleteOnCallSchedule",
        "summary": "Delete an on-call schedule",
        "description": "Refused while a rule's escalation ladder still names the schedule — deleting it would leave that ladder paging nobody. Detach the ladder step first.",
        "tags": [
          "oncall"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "description": "The schedule name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deleted": {
                  "type": "boolean"
                }
              },
              "required": [
                "deleted"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/oncall/schedules/{sid}/current": {
      "get": {
        "operationId": "onCallCurrent",
        "summary": "Who is on call right now",
        "description": "Resolves the schedule's layers and any active override against the current time, and answers with the account and its email — or on_call=false when the rotation leaves nobody covering this moment.",
        "tags": [
          "oncall"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "description": "The schedule name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success."
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary/current \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\"",
        "x-pingtower-unresolved": [
          "response:200"
        ]
      }
    },
    "/v1/projects/{id}/oncall/schedules/{sid}/overrides": {
      "get": {
        "operationId": "listOnCallOverrides",
        "summary": "List a schedule's overrides",
        "description": "Every override on the schedule, past and future.",
        "tags": [
          "oncall"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "description": "The schedule name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "overrides": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "account_id": {
                        "type": "string"
                      },
                      "created_at": {
                        "type": "integer"
                      },
                      "created_by": {
                        "type": "string"
                      },
                      "ends_at": {
                        "type": "integer"
                      },
                      "id": {
                        "type": "string"
                      },
                      "starts_at": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "required": [
                "overrides"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary/overrides \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "post": {
        "operationId": "createOnCallOverride",
        "summary": "Add an on-call override",
        "description": "A time-bounded substitution covering [starts_at, ends_at). The duration is capped: an override is not a way to permanently reassign a schedule — that belongs in the schedule's own layers — so an effectively-permanent window is rejected at write time rather than silently accepted.",
        "tags": [
          "oncall"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "description": "The schedule name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "account_id": {
                "type": "string"
              },
              "ends_at": {
                "type": "integer"
              },
              "starts_at": {
                "type": "integer"
              }
            },
            "required": [
              "account_id"
            ]
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "schema": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                }
              },
              "required": [
                "id"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "422": {
            "description": "The request is well-formed but references something that cannot be used.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary/overrides \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"account_id\":\"<account_id>\",\"starts_at\":\"2026-08-12T09:00:00Z\",\n       \"ends_at\":\"2026-08-12T17:00:00Z\"}'"
      }
    },
    "/v1/projects/{id}/oncall/schedules/{sid}/overrides/{oid}": {
      "delete": {
        "operationId": "deleteOnCallOverride",
        "summary": "Delete an on-call override",
        "description": "Removes the substitution, returning the window to whatever the schedule's layers resolve to.",
        "tags": [
          "oncall"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "oid",
            "in": "path",
            "required": true,
            "description": "The override id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "description": "The schedule name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deleted": {
                  "type": "boolean"
                }
              },
              "required": [
                "deleted"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/projects/demo/oncall/schedules/primary/overrides/<oid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/rename": {
      "post": {
        "operationId": "renameProject",
        "summary": "Rename a project",
        "description": "Changes the name every nested route addresses the project by. Existing ingest tokens keep working — they resolve through the source row, not through the project name. Requires an owner or admin key.",
        "tags": [
          "projects"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project's current name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 128
              }
            },
            "required": [
              "name"
            ]
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "403": {
            "description": "The key's membership role does not permit this operation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-role": "manager",
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/rename \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"name\":\"demo-eu\"}'"
      }
    },
    "/v1/projects/{id}/reports": {
      "get": {
        "operationId": "listReports",
        "summary": "List a project's reports",
        "description": "Every report on the project, with its status and public UUID.",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "reports": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "alert_id": {
                        "type": "string"
                      },
                      "created_at": {
                        "type": "integer"
                      },
                      "created_by": {
                        "type": "string"
                      },
                      "description": {
                        "type": "string"
                      },
                      "id": {
                        "type": "string"
                      },
                      "password_protected": {
                        "type": "boolean"
                      },
                      "project": {
                        "type": "string"
                      },
                      "public_uuid": {
                        "type": "string"
                      },
                      "title": {
                        "type": "string"
                      },
                      "updated_at": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "required": [
                "reports"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/reports \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "post": {
        "operationId": "createReport",
        "summary": "Create an incident report",
        "description": "A report gets an unguessable public UUID and a shareable status page at /public/reports/{uuid}. Setting view_password gates that page behind a bcrypt check; without one, anyone holding the UUID can read it.\n\nalert_id, if set, must name a live alert in the same project.",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "alert_id": {
                "type": "string",
                "minLength": 0,
                "maxLength": 64
              },
              "description": {
                "type": "string",
                "minLength": 0,
                "maxLength": 4096
              },
              "title": {
                "type": "string",
                "minLength": 0,
                "maxLength": 256
              },
              "view_password": {
                "type": "string",
                "minLength": 0,
                "maxLength": 72
              }
            },
            "required": [
              "title"
            ]
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "schema": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "public_uuid": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "public_uuid"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "422": {
            "description": "The request is well-formed but references something that cannot be used.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/reports \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"title\":\"API degraded\",\"status\":\"investigating\"}'"
      }
    },
    "/v1/projects/{id}/reports/{rid}": {
      "get": {
        "operationId": "getReport",
        "summary": "Read one report",
        "description": "The report with its authored entries, newest last.",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "description": "The report id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "entries": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "content_html": {
                        "type": "string"
                      },
                      "content_md": {
                        "type": "string"
                      },
                      "created_at": {
                        "type": "integer"
                      },
                      "created_by": {
                        "type": "string"
                      },
                      "entry_type": {
                        "type": "string"
                      },
                      "id": {
                        "type": "string"
                      },
                      "report_id": {
                        "type": "string"
                      },
                      "title": {
                        "type": "string"
                      }
                    }
                  }
                },
                "report": {
                  "type": "object",
                  "properties": {
                    "alert_id": {
                      "type": "string"
                    },
                    "created_at": {
                      "type": "integer"
                    },
                    "created_by": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "id": {
                      "type": "string"
                    },
                    "password_protected": {
                      "type": "boolean"
                    },
                    "project": {
                      "type": "string"
                    },
                    "public_uuid": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "updated_at": {
                      "type": "integer"
                    }
                  }
                }
              },
              "required": [
                "entries",
                "report"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/reports/<rid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "put": {
        "operationId": "updateReport",
        "summary": "Update a report",
        "description": "view_password is tri-state: omitting the key keeps the current password, an explicit empty string clears it and reopens the public page, and any other value replaces it.",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "description": "The report id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "description": {
                "type": "string",
                "minLength": 0,
                "maxLength": 4096
              },
              "title": {
                "type": "string",
                "minLength": 0,
                "maxLength": 256
              },
              "view_password": {
                "type": "string",
                "minLength": 0,
                "maxLength": 72
              }
            },
            "required": [
              "title"
            ]
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "updated": {
                  "type": "boolean"
                }
              },
              "required": [
                "updated"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X PUT https://api.pingtower.com/v1/projects/demo/reports/<rid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"title\":\"API degraded\",\"status\":\"resolved\"}'"
      },
      "delete": {
        "operationId": "deleteReport",
        "summary": "Delete a report",
        "description": "Removes the report and unpublishes its status page — the public URL 404s from then on.",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "description": "The report id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deleted": {
                  "type": "boolean"
                }
              },
              "required": [
                "deleted"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/projects/demo/reports/<rid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/reports/{rid}/entries": {
      "post": {
        "operationId": "addReportEntry",
        "summary": "Append a report entry",
        "description": "One authored update on the incident timeline. The markdown is rendered to sanitized HTML exactly once, here at write — standard formatting survives, scripts and event handlers do not.",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "description": "The report id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "content_md": {
                "type": "string",
                "minLength": 0,
                "maxLength": 65536
              },
              "entry_type": {
                "type": "string",
                "enum": [
                  "update",
                  "analysis",
                  "resolution"
                ]
              },
              "title": {
                "type": "string",
                "minLength": 0,
                "maxLength": 256
              }
            },
            "required": [
              "content_md"
            ]
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "schema": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                }
              },
              "required": [
                "id"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/reports/<rid>/entries \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"content_md\":\"Root cause identified; rolling back.\"}'"
      }
    },
    "/v1/projects/{id}/reports/{rid}/entries/{eid}": {
      "put": {
        "operationId": "updateReportEntry",
        "summary": "Revise a report entry",
        "description": "Re-renders the entry's markdown through the same sanitizer.",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "eid",
            "in": "path",
            "required": true,
            "description": "The entry id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "description": "The report id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "content_md": {
                "type": "string",
                "minLength": 0,
                "maxLength": 65536
              },
              "entry_type": {
                "type": "string",
                "enum": [
                  "update",
                  "analysis",
                  "resolution"
                ]
              },
              "title": {
                "type": "string",
                "minLength": 0,
                "maxLength": 256
              }
            },
            "required": [
              "content_md"
            ]
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "updated": {
                  "type": "boolean"
                }
              },
              "required": [
                "updated"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X PUT https://api.pingtower.com/v1/projects/demo/reports/<rid>/entries/<eid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"content_md\":\"Rollback complete; monitoring.\"}'"
      },
      "delete": {
        "operationId": "deleteReportEntry",
        "summary": "Delete a report entry",
        "description": "Removes the entry from the public timeline.",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "eid",
            "in": "path",
            "required": true,
            "description": "The entry id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "description": "The report id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deleted": {
                  "type": "boolean"
                }
              },
              "required": [
                "deleted"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/projects/demo/reports/<rid>/entries/<eid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/reports/{rid}/rotate-uuid": {
      "post": {
        "operationId": "rotateReportUUID",
        "summary": "Rotate a report's public link",
        "description": "The response to a leaked status-page URL. A fresh UUID is registered, the report re-keyed, and the old UUID dropped, so every copy of the old link 404s immediately.",
        "tags": [
          "reports"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rid",
            "in": "path",
            "required": true,
            "description": "The report id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "public_uuid": {
                  "type": "string"
                }
              },
              "required": [
                "public_uuid"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/reports/<rid>/rotate-uuid \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/rules": {
      "get": {
        "operationId": "listRules",
        "summary": "List a project's rules",
        "description": "Every rule in the project, with its match condition, renotify interval, and escalation ladder.",
        "tags": [
          "rules"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "rules": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "actions": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "auto_resolve_quiet_minutes": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "conditions": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "key": {
                              "type": "string"
                            },
                            "op": {
                              "type": "string",
                              "enum": [
                                "eq",
                                "neq",
                                "gte",
                                "lte"
                              ]
                            },
                            "value": {}
                          },
                          "required": [
                            "key"
                          ]
                        }
                      },
                      "created_at": {
                        "type": "integer"
                      },
                      "dedup_window_minutes": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "escalations": {
                        "type": "array",
                        "maxItems": 10,
                        "items": {
                          "type": "object",
                          "properties": {
                            "accounts": {
                              "type": "array",
                              "maxItems": 20,
                              "items": {
                                "type": "string",
                                "maxLength": 128
                              }
                            },
                            "after_minutes": {
                              "type": "integer"
                            },
                            "repeat_minutes": {
                              "type": "integer",
                              "minimum": 0
                            },
                            "schedules": {
                              "type": "array",
                              "maxItems": 20,
                              "items": {
                                "type": "string",
                                "maxLength": 200
                              }
                            }
                          }
                        }
                      },
                      "fire_after": {
                        "type": "object",
                        "properties": {
                          "count": {
                            "type": "integer",
                            "minimum": 1
                          },
                          "distinct_sources": {
                            "type": "boolean"
                          },
                          "window_minutes": {
                            "type": "integer",
                            "minimum": 1
                          }
                        }
                      },
                      "identifier_keys": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "investigate": {
                        "type": "boolean"
                      },
                      "match": {
                        "type": "object",
                        "properties": {
                          "min_level": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "sources": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "tags": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "template_contains": {
                            "type": "string",
                            "maxLength": 500
                          }
                        }
                      },
                      "max_renotify": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "name": {
                        "type": "string",
                        "maxLength": 200
                      },
                      "renotify_minutes": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "updated_at": {
                        "type": "integer"
                      }
                    },
                    "required": [
                      "name"
                    ]
                  }
                }
              },
              "required": [
                "rules"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/rules \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "post": {
        "operationId": "createRule",
        "summary": "Create a rule",
        "description": "A rule turns matching events into alerts. Any account a ladder step names is checked against the tenant's memberships at write time: a ladder routing through a non-member would silently page nobody, so it is rejected with a 422 instead.",
        "tags": [
          "rules"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "actions": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "auto_resolve_quiet_minutes": {
                "type": "integer",
                "minimum": 0
              },
              "conditions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "op": {
                      "type": "string",
                      "enum": [
                        "eq",
                        "neq",
                        "gte",
                        "lte"
                      ]
                    },
                    "value": {}
                  },
                  "required": [
                    "key"
                  ]
                }
              },
              "dedup_window_minutes": {
                "type": "integer",
                "minimum": 0
              },
              "escalations": {
                "type": "array",
                "maxItems": 10,
                "items": {
                  "type": "object",
                  "properties": {
                    "accounts": {
                      "type": "array",
                      "maxItems": 20,
                      "items": {
                        "type": "string",
                        "maxLength": 128
                      }
                    },
                    "after_minutes": {
                      "type": "integer"
                    },
                    "repeat_minutes": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "schedules": {
                      "type": "array",
                      "maxItems": 20,
                      "items": {
                        "type": "string",
                        "maxLength": 200
                      }
                    }
                  }
                }
              },
              "fire_after": {
                "type": "object",
                "properties": {
                  "count": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "distinct_sources": {
                    "type": "boolean"
                  },
                  "window_minutes": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              },
              "identifier_keys": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "investigate": {
                "type": "boolean"
              },
              "match": {
                "type": "object",
                "properties": {
                  "min_level": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "sources": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "template_contains": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              },
              "max_renotify": {
                "type": "integer",
                "minimum": 0
              },
              "name": {
                "type": "string",
                "maxLength": 200
              },
              "renotify_minutes": {
                "type": "integer",
                "minimum": 0
              }
            },
            "required": [
              "name"
            ]
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "schema": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "auto_resolve_quiet_minutes": {
                  "type": "integer",
                  "minimum": 0
                },
                "conditions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "key": {
                        "type": "string"
                      },
                      "op": {
                        "type": "string",
                        "enum": [
                          "eq",
                          "neq",
                          "gte",
                          "lte"
                        ]
                      },
                      "value": {}
                    },
                    "required": [
                      "key"
                    ]
                  }
                },
                "created_at": {
                  "type": "integer"
                },
                "dedup_window_minutes": {
                  "type": "integer",
                  "minimum": 0
                },
                "escalations": {
                  "type": "array",
                  "maxItems": 10,
                  "items": {
                    "type": "object",
                    "properties": {
                      "accounts": {
                        "type": "array",
                        "maxItems": 20,
                        "items": {
                          "type": "string",
                          "maxLength": 128
                        }
                      },
                      "after_minutes": {
                        "type": "integer"
                      },
                      "repeat_minutes": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "schedules": {
                        "type": "array",
                        "maxItems": 20,
                        "items": {
                          "type": "string",
                          "maxLength": 200
                        }
                      }
                    }
                  }
                },
                "fire_after": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "distinct_sources": {
                      "type": "boolean"
                    },
                    "window_minutes": {
                      "type": "integer",
                      "minimum": 1
                    }
                  }
                },
                "identifier_keys": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "investigate": {
                  "type": "boolean"
                },
                "match": {
                  "type": "object",
                  "properties": {
                    "min_level": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "sources": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "template_contains": {
                      "type": "string",
                      "maxLength": 500
                    }
                  }
                },
                "max_renotify": {
                  "type": "integer",
                  "minimum": 0
                },
                "name": {
                  "type": "string",
                  "maxLength": 200
                },
                "renotify_minutes": {
                  "type": "integer",
                  "minimum": 0
                },
                "updated_at": {
                  "type": "integer"
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "422": {
            "description": "The request is well-formed but references something that cannot be used.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/rules \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"name\":\"errors\",\"match\":{\"min_level\":400},\"renotify_minutes\":15}'"
      }
    },
    "/v1/projects/{id}/rules/{name}": {
      "get": {
        "operationId": "getRule",
        "summary": "Read one rule",
        "description": "The rule as stored, including any escalation ladder.",
        "tags": [
          "rules"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The rule name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "auto_resolve_quiet_minutes": {
                  "type": "integer",
                  "minimum": 0
                },
                "conditions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "key": {
                        "type": "string"
                      },
                      "op": {
                        "type": "string",
                        "enum": [
                          "eq",
                          "neq",
                          "gte",
                          "lte"
                        ]
                      },
                      "value": {}
                    },
                    "required": [
                      "key"
                    ]
                  }
                },
                "created_at": {
                  "type": "integer"
                },
                "dedup_window_minutes": {
                  "type": "integer",
                  "minimum": 0
                },
                "escalations": {
                  "type": "array",
                  "maxItems": 10,
                  "items": {
                    "type": "object",
                    "properties": {
                      "accounts": {
                        "type": "array",
                        "maxItems": 20,
                        "items": {
                          "type": "string",
                          "maxLength": 128
                        }
                      },
                      "after_minutes": {
                        "type": "integer"
                      },
                      "repeat_minutes": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "schedules": {
                        "type": "array",
                        "maxItems": 20,
                        "items": {
                          "type": "string",
                          "maxLength": 200
                        }
                      }
                    }
                  }
                },
                "fire_after": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "distinct_sources": {
                      "type": "boolean"
                    },
                    "window_minutes": {
                      "type": "integer",
                      "minimum": 1
                    }
                  }
                },
                "identifier_keys": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "investigate": {
                  "type": "boolean"
                },
                "match": {
                  "type": "object",
                  "properties": {
                    "min_level": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "sources": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "template_contains": {
                      "type": "string",
                      "maxLength": 500
                    }
                  }
                },
                "max_renotify": {
                  "type": "integer",
                  "minimum": 0
                },
                "name": {
                  "type": "string",
                  "maxLength": 200
                },
                "renotify_minutes": {
                  "type": "integer",
                  "minimum": 0
                },
                "updated_at": {
                  "type": "integer"
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/rules/errors \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "put": {
        "operationId": "updateRule",
        "summary": "Update a rule",
        "description": "A rule's name is immutable and comes from the path. Sending a different name in the body is rejected rather than treated as a rename; delete and recreate instead.",
        "tags": [
          "rules"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The rule name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "actions": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "auto_resolve_quiet_minutes": {
                "type": "integer",
                "minimum": 0
              },
              "conditions": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "op": {
                      "type": "string",
                      "enum": [
                        "eq",
                        "neq",
                        "gte",
                        "lte"
                      ]
                    },
                    "value": {}
                  },
                  "required": [
                    "key"
                  ]
                }
              },
              "dedup_window_minutes": {
                "type": "integer",
                "minimum": 0
              },
              "escalations": {
                "type": "array",
                "maxItems": 10,
                "items": {
                  "type": "object",
                  "properties": {
                    "accounts": {
                      "type": "array",
                      "maxItems": 20,
                      "items": {
                        "type": "string",
                        "maxLength": 128
                      }
                    },
                    "after_minutes": {
                      "type": "integer"
                    },
                    "repeat_minutes": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "schedules": {
                      "type": "array",
                      "maxItems": 20,
                      "items": {
                        "type": "string",
                        "maxLength": 200
                      }
                    }
                  }
                }
              },
              "fire_after": {
                "type": "object",
                "properties": {
                  "count": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "distinct_sources": {
                    "type": "boolean"
                  },
                  "window_minutes": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              },
              "identifier_keys": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "investigate": {
                "type": "boolean"
              },
              "match": {
                "type": "object",
                "properties": {
                  "min_level": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "sources": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "template_contains": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              },
              "max_renotify": {
                "type": "integer",
                "minimum": 0
              },
              "name": {
                "type": "string",
                "maxLength": 200
              },
              "renotify_minutes": {
                "type": "integer",
                "minimum": 0
              }
            },
            "required": [
              "name"
            ]
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "actions": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "auto_resolve_quiet_minutes": {
                  "type": "integer",
                  "minimum": 0
                },
                "conditions": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "key": {
                        "type": "string"
                      },
                      "op": {
                        "type": "string",
                        "enum": [
                          "eq",
                          "neq",
                          "gte",
                          "lte"
                        ]
                      },
                      "value": {}
                    },
                    "required": [
                      "key"
                    ]
                  }
                },
                "created_at": {
                  "type": "integer"
                },
                "dedup_window_minutes": {
                  "type": "integer",
                  "minimum": 0
                },
                "escalations": {
                  "type": "array",
                  "maxItems": 10,
                  "items": {
                    "type": "object",
                    "properties": {
                      "accounts": {
                        "type": "array",
                        "maxItems": 20,
                        "items": {
                          "type": "string",
                          "maxLength": 128
                        }
                      },
                      "after_minutes": {
                        "type": "integer"
                      },
                      "repeat_minutes": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "schedules": {
                        "type": "array",
                        "maxItems": 20,
                        "items": {
                          "type": "string",
                          "maxLength": 200
                        }
                      }
                    }
                  }
                },
                "fire_after": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "distinct_sources": {
                      "type": "boolean"
                    },
                    "window_minutes": {
                      "type": "integer",
                      "minimum": 1
                    }
                  }
                },
                "identifier_keys": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "investigate": {
                  "type": "boolean"
                },
                "match": {
                  "type": "object",
                  "properties": {
                    "min_level": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "sources": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "template_contains": {
                      "type": "string",
                      "maxLength": 500
                    }
                  }
                },
                "max_renotify": {
                  "type": "integer",
                  "minimum": 0
                },
                "name": {
                  "type": "string",
                  "maxLength": 200
                },
                "renotify_minutes": {
                  "type": "integer",
                  "minimum": 0
                },
                "updated_at": {
                  "type": "integer"
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "422": {
            "description": "The request is well-formed but references something that cannot be used.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X PUT https://api.pingtower.com/v1/projects/demo/rules/errors \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"match\":{\"min_level\":500},\"renotify_minutes\":5}'"
      },
      "delete": {
        "operationId": "deleteRule",
        "summary": "Delete a rule",
        "description": "Stops the rule opening further alerts. Alerts it already opened stay where they are.",
        "tags": [
          "rules"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The rule name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deleted": {
                  "type": "boolean"
                }
              },
              "required": [
                "deleted"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/projects/demo/rules/errors \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/sources": {
      "get": {
        "operationId": "listSources",
        "summary": "List a project's sources",
        "description": "Each source is one named event producer. The listing never returns a source's ingest token — that is shown once, when the source is created or its token is rotated.",
        "tags": [
          "sources"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "sources": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "created_at": {
                        "type": "integer"
                      },
                      "name": {
                        "type": "string"
                      },
                      "retain_logs": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              },
              "required": [
                "sources"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/sources \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "post": {
        "operationId": "createSource",
        "summary": "Create a source",
        "description": "Mints the source's pti_ ingest token and returns it once. Store it when you see it; there is no route that reads it back, only rotate-token, which replaces it.\n\nretain_logs decides whether this source's events are kept for GET /v1/projects/{id}/logtail. Omitted on create it means \"do not retain\", which is why it is optional here and required on update.",
        "tags": [
          "sources"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 128
              },
              "retain_logs": {
                "type": "boolean"
              }
            },
            "required": [
              "name"
            ]
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "schema": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "token": {
                  "type": "string"
                }
              },
              "required": [
                "name",
                "token"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/sources \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"name\":\"api\",\"retain_logs\":true}'"
      }
    },
    "/v1/projects/{id}/sources/{name}": {
      "put": {
        "operationId": "updateSource",
        "summary": "Update a source",
        "description": "retain_logs must be sent explicitly. An omitted field is rejected with a 400 rather than read as false, which would otherwise turn log retention off by accident on any partial update.",
        "tags": [
          "sources"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The source name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "retain_logs": {
                "type": "boolean"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "retain_logs": {
                  "type": "boolean"
                }
              },
              "required": [
                "name",
                "retain_logs"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X PUT https://api.pingtower.com/v1/projects/demo/sources/api \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"retain_logs\":false}'"
      },
      "delete": {
        "operationId": "deleteSource",
        "summary": "Delete a source",
        "description": "Retires the source and its ingest token. Anything still holding that token starts failing authentication on POST /v1/ingest.",
        "tags": [
          "sources"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The source name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deleted": {
                  "type": "boolean"
                }
              },
              "required": [
                "deleted"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/projects/demo/sources/api \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/sources/{name}/rotate-token": {
      "post": {
        "operationId": "rotateSourceToken",
        "summary": "Rotate a source's ingest token",
        "description": "Issues a new pti_ token and returns it once, invalidating the previous one immediately — this is the response to a leaked token, so there is no overlap window. Requires an owner or admin key.",
        "tags": [
          "sources"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "description": "The source name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "token": {
                  "type": "string"
                }
              },
              "required": [
                "name",
                "token"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "403": {
            "description": "The key's membership role does not permit this operation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-role": "manager",
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/sources/api/rotate-token \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/projects/{id}/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "summary": "List delivery endpoints",
        "description": "Every endpoint on the project, with its enabled state and failure streak.",
        "tags": [
          "webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "endpoints": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "consecutive_failures": {
                        "type": "integer"
                      },
                      "created_at": {
                        "type": "integer"
                      },
                      "enabled": {
                        "type": "boolean"
                      },
                      "event_filter": {
                        "type": "string"
                      },
                      "id": {
                        "type": "string"
                      },
                      "last_error": {
                        "type": "string"
                      },
                      "project": {
                        "type": "string"
                      },
                      "url": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "required": [
                "endpoints"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/projects/demo/webhooks \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "post": {
        "operationId": "createWebhook",
        "summary": "Create a delivery endpoint",
        "description": "The plain endpoint surface over the shared delivery outbox. Omitting secret makes the server mint one and return it once; omitting events subscribes the endpoint to every event.\n\nA row created here is the same row the typed integrations routes see — the two families are two views onto one table, not two schemas.",
        "tags": [
          "webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "events": {
                "type": "array",
                "maxItems": 16,
                "items": {
                  "type": "string",
                  "maxLength": 64
                }
              },
              "secret": {
                "type": "string",
                "maxLength": 256
              },
              "url": {
                "type": "string",
                "format": "uri",
                "maxLength": 2048
              }
            },
            "required": [
              "url"
            ]
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "schema": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "secret": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "secret",
                "url"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/projects/demo/webhooks \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"url\":\"https://example.internal/hooks/pingtower\"}'"
      }
    },
    "/v1/projects/{id}/webhooks/{wid}": {
      "patch": {
        "operationId": "updateWebhook",
        "summary": "Enable or disable a delivery endpoint",
        "description": "enabled must be sent explicitly — an omitted field is rejected rather than read as false, which would disable the endpoint by accident. Re-enabling clears the endpoint's failure streak, which is the only way to recover a circuit-broken endpoint short of deleting it.",
        "tags": [
          "webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "wid",
            "in": "path",
            "required": true,
            "description": "The delivery endpoint id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "enabled": {
                  "type": "boolean"
                }
              },
              "required": [
                "enabled"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X PATCH https://api.pingtower.com/v1/projects/demo/webhooks/<wid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"enabled\":true}'"
      },
      "delete": {
        "operationId": "deleteWebhook",
        "summary": "Delete a delivery endpoint",
        "description": "Removes the endpoint. Deliveries already queued for it are dropped.",
        "tags": [
          "webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "wid",
            "in": "path",
            "required": true,
            "description": "The delivery endpoint id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "deleted": {
                  "type": "boolean"
                }
              },
              "required": [
                "deleted"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/projects/demo/webhooks/<wid> \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/pull": {
      "get": {
        "operationId": "pull",
        "summary": "Long-poll the alert stream",
        "description": "Returns alerts newer than cursor, in id order, and the cursor to pass next. With wait set, the request parks until an alert arrives or the wait elapses, so a poller costs one connection rather than a busy loop. The tenant's store is held for the whole park, including the wait.",
        "tags": [
          "alerts"
        ],
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The last alert id already seen; 0 starts from the beginning.",
            "schema": {
              "type": "integer",
              "format": "int64",
              "default": 0,
              "minimum": 0
            }
          },
          {
            "name": "wait",
            "in": "query",
            "required": false,
            "description": "Seconds to park waiting for a new alert. 0 returns immediately.",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0,
              "maximum": 60
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "alerts": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "count": {
                        "type": "integer"
                      },
                      "escalation_level": {
                        "type": "integer"
                      },
                      "first_seen": {
                        "type": "integer"
                      },
                      "id": {
                        "type": "string"
                      },
                      "identifiers": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        }
                      },
                      "investigate": {
                        "type": "boolean"
                      },
                      "last_message": {
                        "type": "string"
                      },
                      "last_seen": {
                        "type": "integer"
                      },
                      "level": {
                        "type": "integer"
                      },
                      "page_accounts": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "project": {
                        "type": "string"
                      },
                      "rule": {
                        "type": "string"
                      },
                      "state": {
                        "type": "string"
                      },
                      "template": {
                        "type": "string"
                      },
                      "updated_seq": {
                        "type": "integer"
                      }
                    }
                  }
                },
                "config_versions": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "integer"
                  }
                },
                "cursor": {
                  "type": "integer"
                }
              },
              "required": [
                "alerts",
                "config_versions",
                "cursor"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl \"https://api.pingtower.com/v1/pull?cursor=0&wait=25\" \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      }
    },
    "/v1/relay": {
      "get": {
        "operationId": "getRelay",
        "summary": "Read whether this box can page a phone",
        "description": "Answers whether a relay credential has been configured on this tenant, and the URL its alerts are sent to. The secret is never returned.\n\nIt exists so \"does this page anybody?\" is a question with an answer. A ladder rung that pages nobody looks identical to one that works, and the version of that discovery nobody wants is the one made during an incident.\n\nA tenant hosted by pingtower.com answers `configured: false` and always will: it has no box, and its pushes go out through its own apns integration instead.",
        "tags": [
          "relay"
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "configured": {
                  "type": "boolean"
                },
                "name": {
                  "type": "string"
                },
                "relay_url": {
                  "type": "string"
                }
              },
              "required": [
                "configured",
                "name",
                "relay_url"
              ]
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-example": "curl https://box.example.com/v1/relay \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\""
      },
      "put": {
        "operationId": "setRelay",
        "summary": "Configure where this box sends alerts to be paged",
        "description": "Records the relay credential pingtower.com minted at pairing, and wires every project on this tenant to it — the ones that exist now, and the ones created afterwards, which inherit it at creation.\n\nThat second half is the point of the route. Delivery endpoints are per project, so configuring a relay by hand would mean pasting a credential once per project, and a project created next week would page nobody with nothing to say so. The endpoint it writes is an ordinary webhook integration named \"pingtower.com relay\": same outbox, same HMAC signature, same retry and circuit breaker as any endpoint configured by hand.\n\nThe app calls this, not a person. It holds both ends of the pairing moment — the code it just redeemed here, and the credential it just received from pingtower.com — so no operator copies a secret between two web UIs.\n\nManager-gated: it decides where this tenant's alerts go to become pages, which is the same rank that decides who those pages reach. Writing it again replaces the credential rather than adding a second relay, so rotating a compromised key is this call with the new secret.\n\nThe secret is write-only. No read route selects it, so a box member cannot recover it from a project they can otherwise see.\n\n`hosted_tenant_id` and `box_public_url` are the other half of the same link, and both are optional. They tell the box which tenant it is on pingtower.com and the URL pingtower.com reaches it at — two facts a box cannot derive, since the ids are separate namespaces and a box behind a proxy does not reliably know the name it is reached by. `pingtower pair` reads them back to print a single join code instead of leaving an operator to read out three strings. Neither is a secret: knowing a tenant id grants nothing, because every pairing but a tenant's first must survive the verify callback.\n\nThey are recorded only when both are present, so a half-written counterpart cannot produce a join code that resolves to the wrong place. Omitting them leaves whatever was recorded before untouched.",
        "tags": [
          "relay"
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "box_public_url": {
                "type": "string"
              },
              "hosted_tenant_id": {
                "type": "string",
                "maxLength": 64
              },
              "relay_secret": {
                "type": "string",
                "minLength": 32,
                "maxLength": 128
              },
              "relay_url": {
                "type": "string"
              }
            },
            "required": [
              "relay_secret"
            ]
          }
        },
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "relay_url": {
                  "type": "string"
                }
              },
              "required": [
                "relay_url"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "403": {
            "description": "The key's membership role does not permit this operation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "429": {
            "description": "The tenant is over its plan's rate limit; see Retry-After.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "apiKeyBearer": []
          }
        ],
        "x-pingtower-role": "manager",
        "x-pingtower-example": "curl -X PUT https://box.example.com/v1/relay \\\n  -H \"Authorization: Bearer ptk_<tenant>_...\" \\\n  -d '{\"relay_url\":\"https://api.pingtower.com/v1/relay/rk_<id>\",\n       \"relay_secret\":\"<48 hex>\",\n       \"hosted_tenant_id\":\"<hosted tenant>\",\n       \"box_public_url\":\"https://box.example.com\"}'"
      }
    },
    "/v1/relay/{key_id}": {
      "post": {
        "operationId": "relay",
        "summary": "Relay a self-hosted alert to its tenant's phones",
        "description": "The one inbound route a self-hosted box calls, and the reason self-hosting can page a phone at all. The box's rule fires, its ladder resolves, its outbox delivers the resulting event here, and pingtower.com sends the push under the operator's APNs credential — which is the one thing a box cannot do for itself, because Apple accepts a push for an app's bundle id only from a key issued under the team that owns it.\n\nClients do not call this. A box does, and it does so as an ordinary HMAC-signed webhook delivery: the body is the same `WebhookEvent` any webhook endpoint receives, signed the same way, retried by the same durable outbox. The relay key id rides in the path because a webhook endpoint holds a URL and a secret and nothing else, so there is nowhere to put a header — which is what keeps the box's half of the relay configuration rather than code.\n\nThe tenant comes from the key id and never from the body. A tenant id read out of relayed JSON would let any box page any tenant's phones.\n\nAn unknown key id, a revoked key id, and a bad signature answer alike, so the route cannot be used to enumerate live keys. A timestamp more than five minutes from now is refused separately — only a caller already holding the secret can reach that check. The body is capped at 64KB and refused on Content-Length before it is read, and each tenant's relay traffic is rate limited on the far side of the key lookup.\n\nDelivery is at-least-once, so the route is idempotent on (tenant, alert, event, timestamp): a replayed delivery answers 200 without sending a second push. A delivery whose push fails answers 5xx *and* forgets the claim, so the box's retry is treated as a first attempt.\n\nWhose phones ring is decided by `box_pairings`, on every event kind. An escalation's `page_accounts` arrive as box account ids and are translated; every other event kind fans out to the tenant's members narrowed to those holding a live pairing. A box account with no live pairing resolves to no devices either way, which is what makes revoking a pairing stop pages immediately.",
        "tags": [
          "relay"
        ],
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "description": "The relay credential's id, as minted at pairing or rotation.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "devices": {
                  "type": "integer"
                },
                "duplicate": {
                  "type": "boolean"
                },
                "relayed": {
                  "type": "boolean"
                }
              },
              "required": [
                "relayed"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "502": {
            "description": "Status 502.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "503": {
            "description": "Status 503.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [],
        "x-pingtower-example": "# A box does this; the URL is what its operator pasted at pairing.\ncurl -X POST https://api.pingtower.com/v1/relay/rk_<id> \\\n  -H \"X-Pingtower-Timestamp: 1754870400\" \\\n  -H \"X-Pingtower-Signature: <hmac-sha256 of ts.body>\" \\\n  -d '{\"event\":\"alert.opened\",\"alert_id\":\"a1\",\"project\":\"api\", ...}'"
      }
    },
    "/v1/tenants/{tid}/pair": {
      "post": {
        "operationId": "tenantPair",
        "summary": "Link this account to a box account",
        "description": "Records that the caller is a particular account on the tenant's box, and therefore that this account's phones may be paged for that tenant. It grants no data access: on a self-hosted tenant that is the box's to grant, which is also why invite and role-change refuse for these tenants.\n\nThe tenant's first pairing is self-authorizing — the account that created the tenant is the one that just set its data plane URL — and is the only one that receives the relay credential, since that credential authorizes paging the whole tenant. Every pairing after it must present a receipt, which pingtower.com redeems against the box before writing anything.\n\nThat redemption runs pingtower.com→box, over public HTTPS, through the SSRF-guarded client. So the requirement it implies is worth stating plainly: a box on a private address or a tailnet can pair the person who created the tenant and nobody after. The phone may be on the tailnet; pingtower.com never is. A team's box needs a publicly resolvable HTTPS endpoint, and the 401 this route answers with says so.",
        "tags": [
          "pairing"
        ],
        "parameters": [
          {
            "name": "tid",
            "in": "path",
            "required": true,
            "description": "The tenant id.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "schema": {
            "type": "object",
            "properties": {
              "box_account_id": {
                "type": "string",
                "maxLength": 128
              },
              "receipt": {
                "type": "string",
                "minLength": 48,
                "maxLength": 48
              }
            },
            "required": [
              "box_account_id",
              "receipt"
            ]
          }
        },
        "responses": {
          "201": {
            "description": "Created."
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "sessionBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/tenants/<tid>/pair \\\n  -H \"Authorization: Bearer pts_...\" \\\n  -d '{\"box_account_id\":\"<box account>\",\"receipt\":\"<48 hex>\"}'",
        "x-pingtower-unresolved": [
          "response:201"
        ]
      }
    },
    "/v1/tenants/{tid}/pairings": {
      "get": {
        "operationId": "listPairings",
        "summary": "List who on this tenant is pageable, and who was",
        "description": "The roster behind the pages. Each row names a hosted account, the box account it is linked to, when the link was made, and `revoked_at` — null while the pairing is live. Revoked rows are returned deliberately: \"was paired and is not any more\" is what an owner auditing the roster is looking for, and omitting them would make a revocation look identical to never having paired.\n\nA box cannot answer this itself. The pairings live on pingtower.com, so a box's own escalation ladder renders names it has no way to resolve a pairing for — a rung that pages nobody looks exactly like one that works. A client holding both hosts joins this list to the box's ladder on `box_account_id` and can say which is which.\n\nVisible to any member rather than to managers only: knowing whether you yourself are pageable is not privileged, and it is the fact a person most needs before the night they are not woken. A hosted tenant has no box, so it answers 409 rather than an empty list, which would read as a tenant that pages nobody.",
        "tags": [
          "pairing"
        ],
        "parameters": [
          {
            "name": "tid",
            "in": "path",
            "required": true,
            "description": "The tenant id.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "pairings": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "account_id": {
                        "type": "string"
                      },
                      "box_account_id": {
                        "type": "string"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "revoked_at": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  }
                }
              },
              "required": [
                "pairings"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "403": {
            "description": "The key's membership role does not permit this operation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "sessionBearer": []
          }
        ],
        "x-pingtower-example": "curl https://api.pingtower.com/v1/tenants/<tid>/pairings \\\n  -H \"Authorization: Bearer pts_...\""
      }
    },
    "/v1/tenants/{tid}/pairings/{aid}": {
      "delete": {
        "operationId": "revokePairing",
        "summary": "Stop paging one person for this tenant",
        "description": "The act that stops the pages, and the counterpart to removing someone's box membership, which stops their data access. Neither implies the other: doing only the first leaves a leaver reading and writing the data in silence, and doing only the second leaves them receiving pushes that carry log lines.\n\nIt writes one column on one row and consults nothing else, so nothing about the tenant's schedules, ladders, or store can make it fail. Revoking the last pairing is allowed and means \"this box pages nobody\". Re-pairing restores it.",
        "tags": [
          "pairing"
        ],
        "parameters": [
          {
            "name": "aid",
            "in": "path",
            "required": true,
            "description": "The hosted account id whose pairing is revoked.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          },
          {
            "name": "tid",
            "in": "path",
            "required": true,
            "description": "The tenant id.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "schema": {
              "type": "object",
              "properties": {
                "revoked": {
                  "type": "boolean"
                }
              },
              "required": [
                "revoked"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "403": {
            "description": "The key's membership role does not permit this operation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "sessionBearer": []
          }
        ],
        "x-pingtower-example": "curl -X DELETE https://api.pingtower.com/v1/tenants/<tid>/pairings/<aid> \\\n  -H \"Authorization: Bearer pts_...\""
      }
    },
    "/v1/tenants/{tid}/relay-key": {
      "post": {
        "operationId": "rotateRelayKey",
        "summary": "Rotate the tenant's relay credential",
        "description": "Mints a fresh relay credential and revokes every earlier one — the tenant-wide kill switch for a box that has been compromised or is misbehaving, as against revoking one person's pairing, which is per person and reversible.\n\nThe secret is returned exactly once, and the app is its courier to the box exactly as at pairing. Owner-gated: until the box is reconfigured with the new credential, its pages stop.",
        "tags": [
          "pairing"
        ],
        "parameters": [
          {
            "name": "tid",
            "in": "path",
            "required": true,
            "description": "The tenant id.",
            "schema": {
              "type": "string",
              "format": "hex"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "schema": {
              "type": "object",
              "properties": {
                "relay_key_id": {
                  "type": "string"
                },
                "relay_secret": {
                  "type": "string"
                }
              },
              "required": [
                "relay_key_id",
                "relay_secret"
              ]
            }
          },
          "400": {
            "description": "The request body, a path parameter, or a query parameter failed validation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "401": {
            "description": "The bearer token is missing, malformed, or not recognized.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "403": {
            "description": "The key's membership role does not permit this operation.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "404": {
            "description": "The addressed project or entity does not exist.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "409": {
            "description": "The write conflicts with an entity that already exists.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          },
          "500": {
            "description": "The daemon could not complete the request.",
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "security": [
          {
            "sessionBearer": []
          }
        ],
        "x-pingtower-example": "curl -X POST https://api.pingtower.com/v1/tenants/<tid>/relay-key \\\n  -H \"Authorization: Bearer pts_...\""
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKeyBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ptk_<tenant>_<secret>",
        "description": "A tenant API key. Operations marked x-pingtower-role: manager additionally require the key's live membership role to be owner or admin. See docs/architecture/transport-auth for the plane's boundaries."
      },
      "ingestBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pti_<tenant>_<secret>",
        "description": "A source's ingest token. The tenant segment routes the write; the secret is verified against that tenant's own sources table. See docs/architecture/transport-auth for the plane's boundaries."
      },
      "sessionBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pts_<secret>",
        "description": "An account session token. The account plane only — a session manages tenants and never touches alert data directly. See docs/architecture/transport-auth for the plane's boundaries."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The error body shared by every non-2xx response.",
        "properties": {
          "error": {
            "type": "string",
            "description": "A short summary of what went wrong."
          },
          "errors": {
            "type": "array",
            "description": "Additional problems, present on a 422 that reports more than one.",
            "items": {
              "type": "string"
            }
          },
          "fields": {
            "type": "array",
            "description": "Per-field validation failures, present on a 400 from a body or parameter check.",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                },
                "rule": {
                  "type": "string"
                }
              },
              "required": [
                "field",
                "message",
                "rule"
              ]
            }
          }
        },
        "required": [
          "error"
        ]
      }
    }
  }
}
