{
  "openapi": "3.1.0",
  "info": {
    "title": "AgentViva API",
    "version": "1.0.0",
    "description": "AgentViva watches AI tools, agents and assistants from the outside and answers one question per target: is it alive, and since when. This is the public API: create monitors that run a `page`, `json`, `canary` or `mcp` check on a schedule, read their current state and history, claim alert rights on a public monitor someone else's team owns, and read a monitor's public status with no API key at all. All authenticated request and response bodies are JSON, and every timestamp is Unix milliseconds unless noted otherwise.\n",
    "contact": {
      "email": "support@agentviva.dev"
    },
    "license": {
      "name": "MIT",
      "identifier": "MIT"
    }
  },
  "servers": [
    {
      "url": "https://agentviva.dev"
    }
  ],
  "tags": [
    {
      "name": "Monitors",
      "description": "Create, list, read, update, delete and probe the `page`, `json`, `canary` and `mcp` checks AgentViva runs for you."
    },
    {
      "name": "Decisions",
      "description": "The tools that have not worked for a while, and your answer for each one (keep, false alarm, stop watching, remove from your site)."
    },
    {
      "name": "Changes",
      "description": "The append-only feed of state changes across your team's monitors, safe to replay."
    },
    {
      "name": "Team",
      "description": "Your team's plan, limits and notification channels."
    },
    {
      "name": "Alerts",
      "description": "The log of alert deliveries (webhook and Telegram) sent for your team's state changes."
    },
    {
      "name": "Claims",
      "description": "Claiming alert rights on a public monitor owned by someone else's team."
    },
    {
      "name": "Catalog",
      "description": "Connecting one link to a JSON or CSV list of your tools, so monitors are kept in step with it."
    },
    {
      "name": "Public",
      "description": "Endpoints that need no API key: public status JSON, the SVG badge, the HTML status page, the AgentVivaBot policy page and the health check.\n"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/monitors": {
      "post": {
        "operationId": "createMonitor",
        "summary": "Create a monitor",
        "description": "Creates one monitor. The first probe runs within `interval_sec`, randomised so bulk creations spread out.\n",
        "tags": [
          "Monitors"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MonitorCreate"
              },
              "example": {
                "type": "page",
                "name": "Notion",
                "external_id": "notion",
                "target": {
                  "url": "https://www.notion.so"
                },
                "public": true,
                "public_slug": "notion"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created monitor.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "monitor"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "monitor": {
                      "$ref": "#/components/schemas/Monitor"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "monitor": {
                    "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                    "external_id": "notion",
                    "type": "page",
                    "name": "Notion",
                    "target": {
                      "url": "https://www.notion.so",
                      "method": "GET",
                      "headers": {},
                      "body": null
                    },
                    "expect": {
                      "status": [
                        200,
                        299
                      ],
                      "max_ms": 10000,
                      "rules": [],
                      "canary": null
                    },
                    "interval_sec": 86400,
                    "confirmations": 3,
                    "public": true,
                    "public_slug": "notion",
                    "paused": false,
                    "state": "up",
                    "state_since": 1789200000000,
                    "last_probe_at": 1789286400000,
                    "last_ok_at": 1789286400000,
                    "last_latency_ms": 412,
                    "last_reason": null,
                    "source": null,
                    "tags": [],
                    "decision": null,
                    "created_at": 1789100000000,
                    "updated_at": 1789286400000
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "get": {
        "operationId": "listMonitors",
        "summary": "List monitors",
        "description": "Lists your team's monitors, oldest `id` first unless `sort` is given, optionally filtered.",
        "tags": [
          "Monitors"
        ],
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "down,degraded"
            },
            "description": "Filter to monitors in this state, or in any state of a comma list (`down,degraded`). Every item must be one of `unknown`, `up`, `degraded`, `down`, `blocked`, else `400 invalid_query`.\n"
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "catalog",
                "manual"
              ]
            },
            "description": "`catalog`: the monitors your catalog owns. `manual`: the monitors made by hand or through the API.\n"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "problems",
                "changed",
                "name"
              ]
            },
            "description": "No `sort`: by `id` (order of creation). `problems`: down, degraded, blocked, unknown, up, then the newest `state_since` first. `changed`: the newest `state_since` first. `name`: the name A to Z, ignoring case. With a `sort`, `next_cursor` is positional (opaque); a monitor can be skipped or shown twice when monitors change between pages. A cursor that is not one `next_cursor` gave you is `400 invalid_query`.\n"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/CheckType"
            },
            "description": "Filter to monitors of this check type."
          },
          {
            "name": "external_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter to the monitor with this exact `external_id`."
          },
          {
            "name": "public",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Filter to public (`true`) or private (`false`) monitors."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 100
            },
            "description": "Case-insensitive substring of the monitor's name or target. `%` and `_` match themselves. 1 to 100 characters after trimming; an empty or longer value is `400 invalid_query`. Combines with the other filters.\n"
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30
            },
            "description": "Adds `daily` to every monitor: one entry per UTC day of the last `days` days (today counts as one) on which it was checked, oldest first. Results are kept 30 days, so 30 is the most. A value that is not an integer from 1 to 30 is `400 invalid_query`.\n"
          },
          {
            "name": "for_days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365
            },
            "description": "Keeps the monitors whose current state began at least this many days ago: `state=down&for_days=7` is \"down for 7 days or more\". A monitor never checked has no `state_since` and never matches. A value that is not an integer from 1 to 365 is `400 invalid_query`.\n"
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 32
            },
            "description": "Keeps the monitors with this tag. Capitals are lowered. A value that is not a valid tag is `400 invalid_query`."
          },
          {
            "name": "ids",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "example": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456,6e2f0a3a-9c1d-4a7b-8e12-abcdef654321"
            },
            "description": "A comma list of 1 to 500 monitor ids. Monitors of other teams are left out. A value outside these rules is `400 invalid_query`.\n"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            },
            "description": "Page size, 1-500, default 100. A value below 1 or not a number is `400 invalid_query`; a value above 500 is silently clamped to 500, not rejected.\n"
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `next_cursor`."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of monitors.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "monitors",
                    "next_cursor"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "monitors": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Monitor"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "daily": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/DailyResults"
                                  }
                                ],
                                "description": "Present only when the `days` query parameter is given."
                              }
                            }
                          }
                        ]
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "monitors": [
                    {
                      "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                      "external_id": "notion",
                      "type": "page",
                      "name": "Notion",
                      "target": {
                        "url": "https://www.notion.so",
                        "method": "GET",
                        "headers": {},
                        "body": null
                      },
                      "expect": {
                        "status": [
                          200,
                          299
                        ],
                        "max_ms": 10000,
                        "rules": [],
                        "canary": null
                      },
                      "interval_sec": 86400,
                      "confirmations": 3,
                      "public": true,
                      "public_slug": "notion",
                      "paused": false,
                      "state": "up",
                      "state_since": 1789200000000,
                      "last_probe_at": 1789286400000,
                      "last_ok_at": 1789286400000,
                      "last_latency_ms": 412,
                      "last_reason": null,
                      "source": null,
                      "tags": [],
                      "decision": null,
                      "created_at": 1789100000000,
                      "updated_at": 1789286400000
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/monitors/bulk": {
      "post": {
        "operationId": "bulkUpsertMonitors",
        "summary": "Create or update up to 1,000 monitors",
        "description": "Upserts by `external_id`: an item whose `external_id` matches an existing monitor on your team updates it (fields you send replace the stored ones; state and history are kept), otherwise it is created. Items are processed independently -- one bad item does not fail the batch, and a per-item failure is reported inside `results`, never as the HTTP status of this request. Bulk calls count as one request against the rate limit.\n",
        "tags": [
          "Monitors"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkRequest"
              },
              "example": {
                "monitors": [
                  {
                    "external_id": "notion",
                    "type": "page",
                    "name": "Notion",
                    "target": {
                      "url": "https://www.notion.so"
                    },
                    "public": true,
                    "public_slug": "notion"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per submitted item, in the same order. This is always `200`: a per-item failure is reported inside `results`, not as this response's status.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "results"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BulkResult"
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "results": [
                    {
                      "external_id": "notion",
                      "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                      "outcome": "created"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/monitors/actions": {
      "post": {
        "operationId": "monitorActions",
        "summary": "Pause, resume or delete many monitors",
        "description": "One action for many monitors. `pause` and `resume` do what `PATCH` with `paused` does; `delete` does what `DELETE /v1/monitors/{id}` does (catalog tools included: the tool goes to the catalog's \"not watched\" list, see `GET /v1/catalog/not-watched`). Each id is done on its own; a repeated id is done once. The results come in the order of the ids. An id that is not one of your monitors gets `\"outcome\": \"error\"` with `\"error\": \"not_found\"`, and the others still run. An unknown `action`, no ids or more than 500 is `400 invalid_query`.\n",
        "tags": [
          "Monitors"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MonitorActionRequest"
              },
              "example": {
                "action": "pause",
                "ids": [
                  "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per id, in the same order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "results"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MonitorActionResult"
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "results": [
                    {
                      "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                      "outcome": "paused"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/monitors/{id}": {
      "get": {
        "operationId": "getMonitor",
        "summary": "Get a monitor",
        "description": "Returns one monitor plus its last 30 probe results, oldest first, and `daily`: the last 30 days in the same shape as `GET /v1/monitors?days=30`. An `mcp` monitor also has `last_evidence`: the evidence of its newest check (server, protocol, tools), or `null` before its first `mcp` check (also right after a change of type) and when it cannot be read at that moment; the rest of the answer is not affected.\n",
        "tags": [
          "Monitors"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MonitorId"
          }
        ],
        "responses": {
          "200": {
            "description": "The monitor, with `last_probes` and `daily` (and `last_evidence` for an `mcp` monitor).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "monitor"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "monitor": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Monitor"
                        },
                        {
                          "type": "object",
                          "required": [
                            "last_probes",
                            "daily"
                          ],
                          "properties": {
                            "daily": {
                              "$ref": "#/components/schemas/DailyResults"
                            },
                            "evidence": {
                              "description": "What the newest check saw, public-safe fields only; `null` before the first.",
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/PublicEvidence"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "last_evidence": {
                              "description": "Only on an `mcp` monitor. The evidence of its newest check, or `null`.\n",
                              "oneOf": [
                                {
                                  "$ref": "#/components/schemas/Evidence"
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "last_probes": {
                              "type": "array",
                              "maxItems": 30,
                              "description": "The last 30 probe results for this monitor, oldest first.",
                              "items": {
                                "type": "object",
                                "required": [
                                  "at",
                                  "verdict",
                                  "http_status",
                                  "latency_ms",
                                  "reason"
                                ],
                                "properties": {
                                  "at": {
                                    "type": "integer",
                                    "format": "int64",
                                    "description": "Unix milliseconds when this probe ran."
                                  },
                                  "verdict": {
                                    "$ref": "#/components/schemas/Verdict"
                                  },
                                  "http_status": {
                                    "type": [
                                      "integer",
                                      "null"
                                    ]
                                  },
                                  "latency_ms": {
                                    "type": "integer"
                                  },
                                  "reason": {
                                    "$ref": "#/components/schemas/Reason"
                                  }
                                }
                              }
                            }
                          }
                        }
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "monitor": {
                    "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                    "external_id": "notion",
                    "type": "page",
                    "name": "Notion",
                    "target": {
                      "url": "https://www.notion.so",
                      "method": "GET",
                      "headers": {},
                      "body": null
                    },
                    "expect": {
                      "status": [
                        200,
                        299
                      ],
                      "max_ms": 10000,
                      "rules": [],
                      "canary": null
                    },
                    "interval_sec": 86400,
                    "confirmations": 3,
                    "public": true,
                    "public_slug": "notion",
                    "paused": false,
                    "state": "up",
                    "state_since": 1789200000000,
                    "last_probe_at": 1789286400000,
                    "last_ok_at": 1789286400000,
                    "last_latency_ms": 412,
                    "last_reason": null,
                    "source": null,
                    "tags": [],
                    "decision": null,
                    "created_at": 1789100000000,
                    "updated_at": 1789286400000,
                    "last_probes": [
                      {
                        "at": 1789286400000,
                        "verdict": "up",
                        "http_status": 200,
                        "latency_ms": 412,
                        "reason": null
                      }
                    ],
                    "daily": [
                      {
                        "date": "2026-10-16",
                        "worst": "up"
                      },
                      {
                        "date": "2026-10-17",
                        "worst": "up"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updateMonitor",
        "summary": "Update a monitor",
        "description": "Body is any subset of the create fields, plus `paused`. Changing `interval_sec` or `target` reschedules the next probe. Changing `type` away from `mcp` drops the stored `expect.mcp`; changing it to `mcp` needs `target.method` `POST`, no `target.body` and no `expect.rules`, as on create (send them in the same patch when the stored ones differ). Changing `type` to `canary` on a plan without canary checks is `402 plan_required`, the same rule `POST /v1/monitors` applies. `paused` pauses or resumes probing: a resumed monitor probes within about a minute, bulk upsert never changes it, and a patch that only sets `paused` is allowed on a canary monitor even when the plan no longer includes canary.\n",
        "tags": [
          "Monitors"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MonitorId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MonitorPatch"
              },
              "example": {
                "interval_sec": 43200
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated monitor.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "monitor"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "monitor": {
                      "$ref": "#/components/schemas/Monitor"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "monitor": {
                    "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                    "external_id": "notion",
                    "type": "page",
                    "name": "Notion",
                    "target": {
                      "url": "https://www.notion.so",
                      "method": "GET",
                      "headers": {},
                      "body": null
                    },
                    "expect": {
                      "status": [
                        200,
                        299
                      ],
                      "max_ms": 10000,
                      "rules": [],
                      "canary": null
                    },
                    "interval_sec": 43200,
                    "confirmations": 3,
                    "public": true,
                    "public_slug": "notion",
                    "paused": false,
                    "state": "up",
                    "state_since": 1789200000000,
                    "last_probe_at": 1789286400000,
                    "last_ok_at": 1789286400000,
                    "last_latency_ms": 412,
                    "last_reason": null,
                    "source": null,
                    "tags": [],
                    "decision": null,
                    "created_at": 1789100000000,
                    "updated_at": 1789286500000
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "`error` is `plan_required`: this patch would change the monitor's `type` to `canary` on a plan without canary checks (`plan_limit` does not apply here -- that code is only returned by `POST /v1/monitors` and `POST /v1/monitors/bulk`, which create a new row).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteMonitor",
        "summary": "Delete a monitor",
        "description": "History rows are kept for 30 days and then pruned. Deleting a monitor your catalog owns also puts its tool on the catalog's \"not watched\" list, so the next sync does not create it again (see `GET /v1/catalog/not-watched`).\n",
        "tags": [
          "Monitors"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MonitorId"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/monitors/{id}/probe": {
      "post": {
        "operationId": "probeMonitorNow",
        "summary": "Probe a monitor now",
        "description": "Runs one probe immediately and returns its result. Does not change the schedule. Counts toward the confirmation rule like a scheduled probe.\n",
        "tags": [
          "Monitors"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MonitorId"
          }
        ],
        "responses": {
          "200": {
            "description": "The result of this probe.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "result"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "result": {
                      "$ref": "#/components/schemas/ProbeResult"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "result": {
                    "verdict": "up",
                    "http_status": 200,
                    "latency_ms": 412,
                    "reason": null,
                    "evidence": {
                      "http_status": 200,
                      "final_url": "https://www.notion.so/",
                      "snippet": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/decisions": {
      "get": {
        "operationId": "listDecisions",
        "summary": "List the tools that need a decision",
        "description": "A tool that has not worked for a while needs a person to decide what happens to it. AgentViva lists those tools and records the answer; it never changes your site. A tool is on the `open` list when it is not paused, its state is `down` (or `degraded` with reason `parked`, a domain that now shows a parking page), that state began at least `decide_after_days` days ago (see `PATCH /v1/team`; default 7), and it has no answer that still counts. An answer counts while it was given during the current state; `keep` and `false_alarm` also end after their days. When the state changes, the answer is over, and a tool that fails again comes back to the list after the days. `open` is the list, longest-failing first. `remove_requested` is the tools you sent for removal that still fail (no wait in days): a site or agent that does not take the webhook reads this list and hides them.\n",
        "tags": [
          "Decisions"
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "remove_requested"
              ],
              "default": "open"
            },
            "description": "Which list to read."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            },
            "description": "Page size, 1-500, default 100."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `next_cursor`."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "status",
                    "after_days",
                    "total",
                    "monitors",
                    "next_cursor"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "open",
                        "remove_requested"
                      ]
                    },
                    "after_days": {
                      "type": "integer",
                      "description": "Your team's `decide_after_days`.",
                      "example": 7
                    },
                    "total": {
                      "type": "integer",
                      "description": "How many tools are on this list."
                    },
                    "monitors": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Monitor"
                          },
                          {
                            "type": "object",
                            "required": [
                              "days_in_state",
                              "why",
                              "status_url"
                            ],
                            "properties": {
                              "days_in_state": {
                                "type": "integer",
                                "description": "Whole days in the current state.",
                                "example": 9
                              },
                              "why": {
                                "type": "string",
                                "description": "The reason in plain words."
                              },
                              "status_url": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "format": "uri",
                                "description": "`null` for a private monitor."
                              }
                            }
                          }
                        ]
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "status": "open",
                  "after_days": 7,
                  "total": 1,
                  "monitors": [
                    {
                      "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                      "external_id": "notion",
                      "type": "page",
                      "name": "Notion",
                      "target": {
                        "url": "https://www.notion.so",
                        "method": "GET",
                        "headers": {},
                        "body": null
                      },
                      "expect": {
                        "status": [
                          200,
                          299
                        ],
                        "max_ms": 10000,
                        "rules": [],
                        "canary": null
                      },
                      "interval_sec": 86400,
                      "confirmations": 3,
                      "public": true,
                      "public_slug": "notion",
                      "paused": false,
                      "state": "down",
                      "state_since": 1792900000000,
                      "last_probe_at": 1793600000000,
                      "last_ok_at": 1792890000000,
                      "last_latency_ms": null,
                      "last_reason": "http_5xx",
                      "source": null,
                      "tags": [],
                      "decision": null,
                      "created_at": 1789100000000,
                      "updated_at": 1793600000000,
                      "days_in_state": 8,
                      "why": "The site answers with a server error.",
                      "status_url": "https://agentviva.dev/s/aigregator/notion"
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "decide",
        "summary": "Answer for one or many tools",
        "description": "`keep`: keep watching; the tool leaves the list for `days` (default `decide_after_days`); outcome `kept`, with `until`. `false_alarm`: the same, and the next check runs within about a minute; outcome `false_alarm`, with `until`. `stop`: stop watching, the monitor is deleted as with `DELETE /v1/monitors/{id}` (a catalog tool goes to the not-watched list); outcome `stopped`. `remove`: ask your site to remove the tool with the signed `remove_requested` webhook (the `RemoveRequest` schema) and `status=remove_requested`; outcome `remove_requested`, with `requested_at` and `webhook`. The webhook is sent to your `webhook_url` with the same headers and signature as every webhook, one delivery per tool. Its `id` is the same on every try, so you can ignore a repeat. A failed delivery is tried again by the hourly job (3 tries in all), and only while the answer is still this remove request. It shows in `GET /v1/alerts` with channel `action`. Only your own team gets it, never a team with a claim on the tool. With a catalog, the loop closes by itself: your site hides the tool, the tool leaves your list, and the next sync pauses its monitor. The results come in the order of the ids; an id that is not one of your monitors is `\"outcome\": \"error\"` with `\"error\": \"not_found\"`. An unknown `action`, no ids, more than 500, or `days` outside 1-365 is `400 invalid_query`.\n",
        "tags": [
          "Decisions"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DecisionRequest"
              },
              "example": {
                "action": "keep",
                "ids": [
                  "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456"
                ],
                "days": 14
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per id, in the same order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "results"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DecisionResult"
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "results": [
                    {
                      "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                      "outcome": "kept",
                      "until": 1794800000000
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/changes": {
      "get": {
        "operationId": "listChanges",
        "summary": "List state changes",
        "description": "Returns state changes for your team, oldest first. Store the `at` of the last change you processed and pass it as `since` next time; changes are never edited or deleted inside 90 days, so this feed is safe to replay.\n",
        "tags": [
          "Changes"
        ],
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "Unix milliseconds. Required; a missing or non-numeric value is `400 invalid_query`."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 200
            },
            "description": "Page size, 1-1000, default 200. A value below 1 or not a number is `400 invalid_query`; a value above 1000 is silently clamped to 1000, not rejected.\n"
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `next_cursor`."
          },
          {
            "name": "monitor_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64
            },
            "description": "Only the changes of this monitor. A monitor of another team finds nothing.\n"
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            },
            "description": "`desc` returns the newest changes first; `next_cursor` then walks back in time and `since` stays the lower bound.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of state changes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "changes",
                    "next_cursor"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "changes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StateChange"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "changes": [
                    {
                      "event": "state_change",
                      "id": "6e2f0a3a-9c1d-4a7b-8e12-abcdef654321",
                      "monitor_id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                      "external_id": "notion",
                      "name": "Notion",
                      "from": "up",
                      "to": "down",
                      "at": 1789286400000,
                      "reason": "http_404",
                      "evidence": {
                        "http_status": 404,
                        "final_url": "https://www.notion.so/",
                        "snippet": null,
                        "judge_note": null
                      },
                      "checks": 3,
                      "first_seen_at": 1789282200000,
                      "url": "https://www.notion.so/",
                      "status_url": "https://agentviva.dev/s/aigregator/notion"
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/team": {
      "get": {
        "operationId": "getTeam",
        "summary": "Get your team",
        "description": "Returns your team's plan, monitor limit, monitor counts by state and channel configuration.",
        "tags": [
          "Team"
        ],
        "responses": {
          "200": {
            "description": "Your team.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "team"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "team": {
                      "$ref": "#/components/schemas/Team"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "team": {
                    "name": "aigregator",
                    "slug": "aigregator",
                    "plan": "pro",
                    "monitor_limit": 5000,
                    "monitors_by_state": {
                      "up": 412,
                      "down": 3,
                      "degraded": 1
                    },
                    "telegram_linked": true,
                    "telegram_digest": false,
                    "webhook_configured": true,
                    "decide_after_days": 7,
                    "instant_tags": []
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "patch": {
        "operationId": "updateTeam",
        "summary": "Update your team's notification channels",
        "description": "`decide_after_days` (1-90, default 7) is how many days a tool must not work before it shows on `GET /v1/decisions`. `instant_tags` (the same rules as a monitor's `tags`; `null` or `[]` clears them): with the daily Telegram summary on, tools with one of these tags still get one Telegram message per change. Setting `webhook_url` sends one test delivery before saving and returns `400 webhook_test_failed` if it does not answer 2xx within 5 s; a redirect is not followed and counts as a failure like any other non-2xx. `https` is recommended: the payload is signed, not encrypted, so a plain `http` endpoint exposes the change data (not the secret) to anyone on the network path.\n",
        "tags": [
          "Team"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TeamPatch"
              },
              "example": {
                "webhook_url": "https://hooks.aigregator.com/agentviva",
                "telegram_chat_id": "123456789"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your team, after the update.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "team"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "team": {
                      "$ref": "#/components/schemas/Team"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "team": {
                    "name": "aigregator",
                    "slug": "aigregator",
                    "plan": "pro",
                    "monitor_limit": 5000,
                    "monitors_by_state": {
                      "up": 412,
                      "down": 3,
                      "degraded": 1
                    },
                    "telegram_linked": true,
                    "telegram_digest": false,
                    "webhook_configured": true,
                    "decide_after_days": 7,
                    "instant_tags": []
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/team/webhook/test": {
      "post": {
        "operationId": "testTeamWebhook",
        "summary": "Send a test delivery to your webhook",
        "description": "No body. Sends one test delivery to the webhook you saved, the same `{ \"test\": true, \"team\": \"<slug>\" }` body that setting `webhook_url` sends, signed the same way. Returns `200` when it answers 2xx within 5 s, `400 webhook_test_failed` when it does not, and `400 invalid_query` when no webhook is set.\n",
        "tags": [
          "Team"
        ],
        "responses": {
          "200": {
            "description": "The webhook answered 2xx within 5 s.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/alerts": {
      "get": {
        "operationId": "listAlerts",
        "summary": "List alert deliveries",
        "description": "Returns your team's newest alert deliveries, newest first, kept 90 days. A failed delivery is tried again by the hourly job, 3 tries in all. `name`, `from` and `to` are `null` when the monitor has been deleted. `channel` `action` is a remove request sent to your webhook (see `POST /v1/decisions`); its `from` and `to` are `null`.\n",
        "tags": [
          "Alerts"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            },
            "description": "Page size, 1-100, default 20. A value below 1 or not a number is `400 invalid_query`."
          }
        ],
        "responses": {
          "200": {
            "description": "The newest alert deliveries.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "alerts"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "alerts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Alert"
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "alerts": [
                    {
                      "id": "3d9a6c1e-2b7f-4e55-9a10-abcdef112233",
                      "monitor_id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                      "name": "Jellypod",
                      "own": true,
                      "change_id": "6e2f0a3a-9c1d-4a7b-8e12-abcdef654321",
                      "from": "up",
                      "to": "degraded",
                      "channel": "webhook",
                      "sent": false,
                      "attempts": 1,
                      "error": "webhook 500",
                      "created_at": 1792238900000
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/claims": {
      "post": {
        "operationId": "createClaim",
        "summary": "Claim a public monitor",
        "description": "Gives your team rights over a public monitor owned by someone else -- typically the monitor an AI directory created for your tool. Ownership does not move. Returns a token and both verification methods (DNS TXT record or well-known file). Calling it twice for the same monitor returns the same token, unless the monitor's domain turns out to be unclaimable, which is checked before that idempotent lookup runs.\n",
        "tags": [
          "Claims"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "monitor_id"
                ],
                "properties": {
                  "monitor_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The `id` of a public monitor you do not already own."
                  }
                },
                "example": {
                  "monitor_id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An existing claim for this monitor and team, returned again (idempotent reload).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "claim"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "claim": {
                      "$ref": "#/components/schemas/Claim"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "claim": {
                    "id": "7c1e2b3a-5d4f-4a9c-8b6e-abcdef987654",
                    "monitor_id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                    "domain": "notion.so",
                    "token": "av-verify-9f8e7d6c5b4a3928",
                    "verified": false,
                    "method": null,
                    "verified_at": null,
                    "dns": {
                      "name": "_agentviva.notion.so",
                      "type": "TXT",
                      "value": "av-verify-9f8e7d6c5b4a3928"
                    },
                    "file": {
                      "path": "/.well-known/agentviva-challenge",
                      "body": "av-verify-9f8e7d6c5b4a3928"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "A new claim.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "claim"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "claim": {
                      "$ref": "#/components/schemas/Claim"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "claim": {
                    "id": "7c1e2b3a-5d4f-4a9c-8b6e-abcdef987654",
                    "monitor_id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                    "domain": "notion.so",
                    "token": "av-verify-9f8e7d6c5b4a3928",
                    "verified": false,
                    "method": null,
                    "verified_at": null,
                    "dns": {
                      "name": "_agentviva.notion.so",
                      "type": "TXT",
                      "value": "av-verify-9f8e7d6c5b4a3928"
                    },
                    "file": {
                      "path": "/.well-known/agentviva-challenge",
                      "body": "av-verify-9f8e7d6c5b4a3928"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`error` is `invalid_query` (missing `monitor_id`, or your team already owns this monitor) or `domain_not_claimable` (the monitor's domain is a shared or public suffix nobody can prove they own -- e.g. `*.vercel.app`, `*.github.io`, or a ccTLD second level such as `com.ua`; api.md's own list is illustrative, not exhaustive).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "`error` is `plan_required`: claims need a paid plan. Both current plans (`free` and `pro`) include claims, so this cannot happen today.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`error` is `not_found`: the monitor is private or does not exist -- the two are indistinguishable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "get": {
        "operationId": "listClaims",
        "summary": "List your team's claims",
        "description": "Returns your team's claims, newest first.",
        "tags": [
          "Claims"
        ],
        "responses": {
          "200": {
            "description": "Your team's claims.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "claims"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "claims": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Claim"
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "claims": [
                    {
                      "id": "7c1e2b3a-5d4f-4a9c-8b6e-abcdef987654",
                      "monitor_id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                      "domain": "notion.so",
                      "token": "av-verify-9f8e7d6c5b4a3928",
                      "verified": true,
                      "method": "dns",
                      "verified_at": 1789200000000,
                      "dns": {
                        "name": "_agentviva.notion.so",
                        "type": "TXT",
                        "value": "av-verify-9f8e7d6c5b4a3928"
                      },
                      "file": {
                        "path": "/.well-known/agentviva-challenge",
                        "body": "av-verify-9f8e7d6c5b4a3928"
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/claimable": {
      "get": {
        "operationId": "listClaimable",
        "summary": "Find claimable monitors on a domain",
        "description": "Lists the public monitors of other teams whose target is on the given registrable domain, at most 20, sorted by name. Your own monitors and private monitors are never listed in `monitors`; your own monitors on the domain are listed in `own_monitors`. Use it to find the monitor to pass to `POST /v1/claims`.\n",
        "tags": [
          "Claims"
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "example": "geondex.com"
            },
            "description": "A domain name. A pasted web address works too: the scheme, user info, path, query, port and a leading `www.` are removed and the rest is lower-cased. A malformed or missing value is `400 invalid_query`; a shared or public suffix such as `foo.vercel.app` is `400 domain_not_claimable`.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "The matching monitors, possibly none.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "monitors",
                    "own_monitors"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "domain": {
                      "type": "string",
                      "description": "The registrable domain that was looked up.",
                      "example": "geondex.com"
                    },
                    "monitors": {
                      "type": "array",
                      "maxItems": 20,
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "team",
                          "public_slug",
                          "url",
                          "status_url"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "Pass this as `monitor_id` to `POST /v1/claims`."
                          },
                          "name": {
                            "type": "string"
                          },
                          "team": {
                            "type": "string",
                            "description": "The owning team's slug."
                          },
                          "public_slug": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri",
                            "description": "The monitor's target URL without its query string, fragment and user info (origin and path only), because those can hold a token."
                          },
                          "status_url": {
                            "type": "string",
                            "format": "uri",
                            "description": "The public status page."
                          }
                        }
                      }
                    },
                    "own_monitors": {
                      "type": "array",
                      "maxItems": 20,
                      "description": "Your own monitors on that domain, public or not, sorted by name. You cannot claim them, but a non-empty list tells you the tool is already yours when `monitors` is empty.\n",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "url"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "description": "The monitor's target URL."
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "monitors": [
                    {
                      "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                      "name": "Geondex",
                      "team": "aigregator",
                      "public_slug": "geondex",
                      "url": "https://www.geondex.com/",
                      "status_url": "https://agentviva.dev/s/aigregator/geondex"
                    }
                  ],
                  "own_monitors": []
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/claims/{id}/verify": {
      "post": {
        "operationId": "verifyClaim",
        "summary": "Verify a claim",
        "description": "Checks the domain proof (DNS TXT record first, then the well-known file) and marks the claim verified. A claim is verified once and is never re-checked; calling this again on an already-verified claim returns `200` with the claim unchanged, without re-checking proof.\n",
        "tags": [
          "Claims"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The claim's `id`."
          }
        ],
        "responses": {
          "200": {
            "description": "Verified (or already was).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "claim"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "claim": {
                      "$ref": "#/components/schemas/Claim"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "claim": {
                    "id": "7c1e2b3a-5d4f-4a9c-8b6e-abcdef987654",
                    "monitor_id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
                    "domain": "notion.so",
                    "token": "av-verify-9f8e7d6c5b4a3928",
                    "verified": true,
                    "method": "dns",
                    "verified_at": 1789200000000,
                    "dns": {
                      "name": "_agentviva.notion.so",
                      "type": "TXT",
                      "value": "av-verify-9f8e7d6c5b4a3928"
                    },
                    "file": {
                      "path": "/.well-known/agentviva-challenge",
                      "body": "av-verify-9f8e7d6c5b4a3928"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`error` is `already_claimed`: the domain proof was real, but another team verified a claim on this same monitor first.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`error` is `verification_failed`: no matching DNS TXT record and no matching file were found. `detail` names the exact record and path that were checked.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/claims/{id}": {
      "delete": {
        "operationId": "deleteClaim",
        "summary": "Delete a claim",
        "description": "Drops a claim, freeing the monitor for someone else to claim.",
        "tags": [
          "Claims"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The claim's `id`."
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/catalog": {
      "get": {
        "operationId": "getCatalog",
        "summary": "Get your catalog",
        "description": "Returns the catalog link your team connected, or `null` when there is none.",
        "tags": [
          "Catalog"
        ],
        "responses": {
          "200": {
            "description": "Your catalog, or `null`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "catalog"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "catalog": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Catalog"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "catalog": {
                    "url": "https://aigregator.com/agentviva-feed.json",
                    "public": true,
                    "last_sync_at": null,
                    "next_sync_at": 1792000000000,
                    "last_result": null,
                    "monitors_by_state": {},
                    "not_watched": 0,
                    "down_30_days": 0
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "put": {
        "operationId": "connectCatalog",
        "summary": "Connect (or replace) your catalog",
        "description": "Fetches and reads the list once, saves the link and schedules the first sync for now. No monitor is created or changed by this call. The link must be a public `http` or `https` URL (the same host rule as a monitor's `target.url`, applied to every redirect too) to a JSON or CSV file of at most 10 MB; the fetch waits at most 15 s and follows at most 5 redirects. JSON is an array of objects, or an object whose `tools`, `items`, `monitors` or `data` field is that array. CSV has a header row. The id is the first of `id`, `external_id`, `slug`, `name_slug`; the name is `name` or `title`; the URL is the first of `url`, `website`, `homepage`, `link`, `ai_tool_url`. An optional `tags` field per row is a JSON array of strings, or one string split on `,`, `|` or `;` (a CSV cell such as `featured|partner`); capitals are lowered and spaces trimmed, a tag that is not 1-32 of `a-z 0-9 - _` is left out (the row is still read), at most 10. The list sets the tags of the monitors the catalog owns: a row without tags clears them. A row without an id or a URL, or a repeated id, is counted in `preview.invalid`. A CSV with a quote that is never closed is refused, and so is a list with no valid tool at all.\n",
        "tags": [
          "Catalog"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CatalogPut"
              },
              "example": {
                "url": "https://aigregator.com/agentviva-feed.json",
                "public": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved. `preview` shows what the first sync would read.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "catalog",
                    "preview"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "catalog": {
                      "$ref": "#/components/schemas/Catalog"
                    },
                    "preview": {
                      "type": "object",
                      "required": [
                        "found",
                        "invalid",
                        "sample"
                      ],
                      "properties": {
                        "found": {
                          "type": "integer",
                          "description": "Tools with an id and a URL."
                        },
                        "invalid": {
                          "type": "integer",
                          "description": "Rows skipped for a missing id or URL, or a repeated id."
                        },
                        "sample": {
                          "type": "array",
                          "maxItems": 3,
                          "items": {
                            "$ref": "#/components/schemas/CatalogItem"
                          }
                        },
                        "skipped": {
                          "type": "array",
                          "maxItems": 5,
                          "description": "Up to five of the invalid rows, so the list can be fixed.",
                          "items": {
                            "type": "object",
                            "required": [
                              "row",
                              "name",
                              "reason"
                            ],
                            "properties": {
                              "row": {
                                "type": "integer",
                                "description": "The tool's place in the list, from 1 (a CSV header is not counted)."
                              },
                              "name": {
                                "type": "string",
                                "maxLength": 80,
                                "description": "The name, id or address to recognise the row by."
                              },
                              "reason": {
                                "type": "string",
                                "enum": [
                                  "no id",
                                  "no address",
                                  "id used twice",
                                  "not an object"
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "catalog": {
                    "url": "https://aigregator.com/agentviva-feed.json",
                    "public": true,
                    "last_sync_at": null,
                    "next_sync_at": 1792000000000,
                    "last_result": null,
                    "monitors_by_state": {},
                    "not_watched": 0,
                    "down_30_days": 0
                  },
                  "preview": {
                    "found": 5057,
                    "invalid": 0,
                    "sample": [
                      {
                        "id": "notion",
                        "name": "Notion",
                        "url": "https://notion.so"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "`error` is `invalid_json`, `invalid_query` (the body failed validation), `invalid_target` (a disallowed scheme or private host in `url`) or `catalog_unreadable` (the link could not be fetched or read, or the list holds no valid tool; `detail` says why). Nothing is saved.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "ok": false,
                  "error": "catalog_unreadable",
                  "detail": "the link answered 404"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "disconnectCatalog",
        "summary": "Disconnect your catalog",
        "description": "Removes the link. Every monitor stays as it is. The \"not watched\" list is emptied.\n",
        "tags": [
          "Catalog"
        ],
        "responses": {
          "204": {
            "description": "Removed. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/catalog/sync": {
      "post": {
        "operationId": "syncCatalog",
        "summary": "Sync your catalog now",
        "description": "Schedules a sync for now; it starts within 10 minutes. The optional body `{ \"force\": true }` (\"Apply anyway\") lets the next sync skip the two safety rules once: a list that shrank by more than half, and a sync that would pause more than half of the catalog's running monitors. The force waits for a sync that gets past the other checks; a `PUT` or `DELETE` of the catalog drops it. `404 not_found` when no catalog is connected.\n",
        "tags": [
          "Catalog"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "force": {
                    "type": "boolean",
                    "description": "Skip the shrink and mass-pause rules on the next sync, once."
                  }
                }
              },
              "example": {
                "force": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scheduled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "catalog"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "catalog": {
                      "$ref": "#/components/schemas/Catalog"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/catalog/not-watched": {
      "get": {
        "operationId": "listNotWatched",
        "summary": "List the catalog tools you stopped watching",
        "description": "Deleting a monitor the catalog owns (`DELETE /v1/monitors/{id}`, `POST /v1/monitors/actions` with `delete`, the `stop` answer on `POST /v1/decisions`, MCP `delete_monitors`, or the website) puts its tool on this list. The tool can stay in your list; the sync leaves it out (`last_result.not_watched`) and it does not count toward your monitor limit. To stop only for a while, pause the monitor instead: a paused catalog monitor stays paused at every sync. The list is A to Z by id. `404 not_found` when no catalog is connected.\n",
        "tags": [
          "Catalog"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 100
            },
            "description": "Page size, 1-1000, default 100."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `next_cursor`."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "tools",
                    "next_cursor"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "tools": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "external_id",
                          "created_at"
                        ],
                        "properties": {
                          "external_id": {
                            "type": "string",
                            "example": "notion"
                          },
                          "created_at": {
                            "type": "integer",
                            "format": "int64",
                            "example": 1793600000000
                          }
                        }
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "tools": [
                    {
                      "external_id": "notion",
                      "created_at": 1793600000000
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "watchCatalogToolAgain",
        "summary": "Watch a catalog tool again",
        "description": "Takes the tool off the \"not watched\" list. The next sync is made due now, so the monitor comes back within about 10 minutes. `404 not_found` when the id is not on the list or no catalog is connected; `400` for a bad body.\n",
        "tags": [
          "Catalog"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "external_id"
                ],
                "properties": {
                  "external_id": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 128
                  }
                }
              },
              "example": {
                "external_id": "notion"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Removed from the list. No body."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/public/search": {
      "get": {
        "operationId": "searchPublicTools",
        "summary": "Find public tools AgentViva already watches",
        "description": "No API key needed. Up to 5 public monitors that are not paused: by registrable domain when `q` looks like an address, else by name (an exact name first, then the shortest). It reads only what AgentViva already watches and sends no request to the address. Answers are cached for 5 minutes; callers without a key are limited to 30 requests a minute per IP address (shared with the keyless MCP mode, `/mcp/public`).\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 100
            },
            "description": "A tool name or an address.",
            "example": "notion"
          }
        ],
        "responses": {
          "200": {
            "description": "The matching public tools.",
            "headers": {
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "*"
                  ]
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "public, max-age=300"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "query",
                    "tools"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "query": {
                      "type": "string",
                      "description": "The cleaned query (trimmed, lower case)."
                    },
                    "tools": {
                      "type": "array",
                      "maxItems": 5,
                      "items": {
                        "type": "object",
                        "required": [
                          "name",
                          "team",
                          "state",
                          "since",
                          "last_probe_at",
                          "reason",
                          "url",
                          "status_url",
                          "badge_url",
                          "json_url"
                        ],
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "team": {
                            "type": "string",
                            "description": "The slug of the team that watches it."
                          },
                          "state": {
                            "$ref": "#/components/schemas/MonitorState"
                          },
                          "since": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "format": "int64"
                          },
                          "last_probe_at": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "format": "int64"
                          },
                          "reason": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Why it is not up; `null` when up, except `moved`."
                          },
                          "url": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The site origin of a website check; `null` for an API check."
                          },
                          "status_url": {
                            "type": "string"
                          },
                          "badge_url": {
                            "type": "string"
                          },
                          "json_url": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "query": "notion",
                  "tools": [
                    {
                      "name": "Notion",
                      "team": "aigregator",
                      "state": "up",
                      "since": 1789200000000,
                      "last_probe_at": 1789286400000,
                      "reason": null,
                      "url": "https://www.notion.so",
                      "status_url": "https://agentviva.dev/s/aigregator/notion",
                      "badge_url": "https://agentviva.dev/badge/aigregator/notion.svg",
                      "json_url": "https://agentviva.dev/v1/public/aigregator/notion.json"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/public/check": {
      "get": {
        "operationId": "checkNow",
        "summary": "Check any public address once, right now",
        "description": "No API key needed. One check of a public website (`type=page`, the default) or hosted MCP server (`type=mcp`), right now, with what AgentViva saw. Creates no monitor. A bare domain gets `https://`. A private, local or non-http(s) address is `400 invalid_url` before any request. `state` is one check's verdict, not a confirmed state. The same address and type are answered from a 5-minute copy (`cached: true`). Limited to 10 checks a minute per IP address and a brake per data centre.\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2048
            },
            "example": "v0.dev"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "page",
                "mcp"
              ],
              "default": "page"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What the check saw.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InstantCheck"
                },
                "example": {
                  "ok": true,
                  "url": "https://v0.dev/",
                  "type": "page",
                  "checked_at": 1794643271764,
                  "state": "up",
                  "reason": "moved",
                  "reason_text": "Moved to a new address",
                  "meaning": "It works, at a new address.",
                  "evidence": {
                    "http_status": 200,
                    "latency_ms": 1293,
                    "moved_to": "https://v0.app",
                    "final_host": "v0.app",
                    "redirects": 1,
                    "title": "v0 by Vercel"
                  },
                  "watched": [],
                  "cached": false
                }
              }
            }
          },
          "400": {
            "description": "`invalid_url`: not a public http(s) address; `detail` says why in plain words.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicCheckError"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many checks from this network, or in this data centre.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicCheckError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/public/report": {
      "post": {
        "operationId": "reportProblem",
        "summary": "Report that a watched tool does not work",
        "description": "No API key needed. Finds the best public monitor for `query` (as `GET /v1/public/search` does), stores the report (one per network, tool and UTC day; the IP address is never stored) and checks the tool again, at most once an hour per tool. A report never changes a state by itself. A tool AgentViva does not watch is `found: false`. The limits of `GET /v1/public/check` apply.\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "query"
                ],
                "properties": {
                  "query": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 300,
                    "description": "A tool name or an address."
                  },
                  "note": {
                    "type": "string",
                    "description": "What happened; cut to 200 characters."
                  }
                }
              },
              "example": {
                "query": "notion.so",
                "note": "login fails"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The report, and the newest check.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "ok",
                        "found",
                        "tool",
                        "counted",
                        "rechecked",
                        "check",
                        "reports_7d"
                      ],
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "found": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "tool": {
                          "type": "object",
                          "description": "The tool, in the shape of a `GET /v1/public/search` item."
                        },
                        "counted": {
                          "type": "boolean",
                          "description": "`false` when this network already reported it today."
                        },
                        "rechecked": {
                          "type": "boolean",
                          "description": "`false` when it was re-checked within the last hour."
                        },
                        "check": {
                          "oneOf": [
                            {
                              "type": "object",
                              "properties": {
                                "verdict": {
                                  "type": "string"
                                },
                                "reason": {
                                  "$ref": "#/components/schemas/Reason"
                                }
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "reports_7d": {
                          "type": "integer"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "ok",
                        "found",
                        "query"
                      ],
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "found": {
                          "type": "boolean",
                          "enum": [
                            false
                          ]
                        },
                        "query": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`invalid_query` or `invalid_json`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicCheckError"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicCheckError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/public/tools.json": {
      "get": {
        "operationId": "listPublicTools",
        "summary": "The liveness feed for directories",
        "description": "No API key needed. Every tool on the public list (the same rule as `/status` and the sitemap) with its registrable `domain`, `site` origin (website checks only), state, since when, and its status page, badge and JSON links. Updated hourly. At most 49,000 tools.\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The feed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "generated_at",
                    "count",
                    "tools"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "generated_at": {
                      "type": "integer",
                      "format": "int64"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "tools": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "name",
                          "domain",
                          "site",
                          "state",
                          "since",
                          "reason",
                          "status_url",
                          "badge_url",
                          "json_url"
                        ],
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "domain": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "site": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "state": {
                            "$ref": "#/components/schemas/MonitorState"
                          },
                          "since": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "format": "int64"
                          },
                          "reason": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "`null` when up, except `moved`."
                          },
                          "status_url": {
                            "type": "string"
                          },
                          "badge_url": {
                            "type": "string"
                          },
                          "json_url": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The database did not answer and no recent copy was at hand."
          }
        }
      }
    },
    "/research.json": {
      "get": {
        "operationId": "getResearch",
        "summary": "The state of AI tools, in numbers",
        "description": "No API key needed. Counts over the public monitors of every team that are not paused, have been checked and are at least 14 days old, in any state. No team is named. Computed at most once an hour.\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The numbers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "generated_at",
                    "tools",
                    "by_state",
                    "reasons",
                    "moved",
                    "stopped_by_month",
                    "down_days"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "generated_at": {
                      "type": "integer",
                      "format": "int64"
                    },
                    "tools": {
                      "type": "integer"
                    },
                    "by_state": {
                      "type": "object",
                      "properties": {
                        "up": {
                          "type": "integer"
                        },
                        "degraded": {
                          "type": "integer"
                        },
                        "down": {
                          "type": "integer"
                        },
                        "blocked": {
                          "type": "integer"
                        },
                        "unknown": {
                          "type": "integer"
                        }
                      }
                    },
                    "reasons": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "state": {
                            "type": "string"
                          },
                          "reason": {
                            "type": "string"
                          },
                          "tools": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "moved": {
                      "type": "integer"
                    },
                    "stopped_by_month": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "month": {
                            "type": "string",
                            "example": "2026-11"
                          },
                          "tools": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "down_days": {
                      "type": "object",
                      "properties": {
                        "median": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "over_30": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/public/{team}/{external_id}.json": {
      "get": {
        "operationId": "getPublicStatus",
        "summary": "Get a public monitor's status as JSON",
        "description": "No API key needed. A private or unknown monitor (or unknown team) is `404 not_found` -- the three cases are indistinguishable.\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "team",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The owning team's slug.",
            "example": "aigregator"
          },
          {
            "name": "external_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The monitor's `external_id` (without the `.json` suffix).",
            "example": "notion"
          }
        ],
        "responses": {
          "200": {
            "description": "The monitor's public status.",
            "headers": {
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "*"
                  ]
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "public, max-age=300"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicStatus"
                },
                "example": {
                  "state": "up",
                  "since": 1789200000000,
                  "last_probe_at": 1789286400000,
                  "latency_ms": 412,
                  "reason": null,
                  "evidence": {
                    "http_status": 200,
                    "latency_ms": 412,
                    "title": "Notion"
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "Our database did not answer within 3 s and no recent copy was at hand. When a copy up to 6 hours old exists, it is served with `200` and the header `X-AV-Stale: 1` instead.\n",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "example": "60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "error"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string",
                      "enum": [
                        "unavailable"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/badge/{team}/{public_slug}.svg": {
      "get": {
        "operationId": "getBadge",
        "summary": "Get a live SVG status badge",
        "description": "No API key needed. An SVG with the monitor's state, `Cache-Control: public, max-age=300` on success. There are seven forms (`style`) and three themes (`theme`). Without them you get the dark card, 180x48, as before; the card also shows how long the state has held (for example \"alive 26 days\"). Sizes: dot 14x14, word 116-172x22, pill 147-178x20, card 180x48, seal 96x96, strip 320x68 (with the last 30 days), panel 360x184 (everything). `theme=auto` follows the reader's light or dark setting with `prefers-color-scheme` inside the SVG. GitHub uses its own theme setting, so for a README use a `<picture>` with a `theme=dark` source and a `theme=light` image.\n",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "team",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "aigregator"
          },
          {
            "name": "public_slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]{1,64}$"
            },
            "description": "The monitor's `public_slug` (without the `.svg` suffix).",
            "example": "notion"
          },
          {
            "name": "style",
            "in": "query",
            "required": false,
            "description": "The form of the badge. An unknown value falls back to `card`.",
            "schema": {
              "type": "string",
              "enum": [
                "dot",
                "word",
                "pill",
                "card",
                "seal",
                "strip",
                "panel"
              ],
              "default": "card"
            },
            "example": "pill"
          },
          {
            "name": "theme",
            "in": "query",
            "required": false,
            "description": "The colour theme. An unknown value falls back to `dark`.",
            "schema": {
              "type": "string",
              "enum": [
                "dark",
                "light",
                "auto"
              ],
              "default": "dark"
            },
            "example": "auto"
          }
        ],
        "responses": {
          "200": {
            "description": "The badge.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "public, max-age=300"
                }
              }
            },
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown team, unknown monitor, or a private monitor -- these must be indistinguishable, so the body is still a rendered badge (state `unknown`), not a JSON error, and `Cache-Control` is not set.\n",
            "content": {
              "image/svg+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/status": {
      "get": {
        "operationId": "getToolList",
        "summary": "Get the public list of tools",
        "description": "No API key needed. An A to Z menu and the tools not working right now. Lists a tool when its monitor is public, not paused, has been checked, is up, degraded or down, and was created at least 14 days ago. No JavaScript.",
        "tags": [
          "Public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The list page.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "public, max-age=300"
                }
              }
            },
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/status/{letter}": {
      "get": {
        "operationId": "getToolListByLetter",
        "summary": "Get the public tools of one first letter",
        "description": "No API key needed. The tools whose names start with this letter, by name. No JavaScript.",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "letter",
            "in": "path",
            "required": true,
            "description": "A lower-case letter, or `other` for a name that starts with a digit or another alphabet.",
            "schema": {
              "type": "string",
              "pattern": "^([a-z]|other)$"
            },
            "example": "n"
          }
        ],
        "responses": {
          "200": {
            "description": "The list page.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "public, max-age=300"
                }
              }
            },
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Not a valid letter, or no tool to list under it. An HTML page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/s/{team}/{public_slug}": {
      "get": {
        "operationId": "getStatusPage",
        "summary": "Get a public HTML status page",
        "description": "No API key needed. 30-day daily-worst-verdict history, no JavaScript.",
        "tags": [
          "Public"
        ],
        "security": [],
        "parameters": [
          {
            "name": "team",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "aigregator"
          },
          {
            "name": "public_slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]{1,64}$"
            },
            "example": "notion"
          }
        ],
        "responses": {
          "200": {
            "description": "The status page.",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "public, max-age=300"
                }
              }
            },
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown team or unknown monitor. Plain text, not JSON. Unlike the badge and public JSON endpoints, this route does not filter on `public = 1` before the \"unknown team\" check, but `getMonitorByPublicSlug` itself only ever resolves a public monitor, so a private monitor's slug 404s the same as an unknown one.\n",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "not found"
                }
              }
            }
          }
        }
      }
    },
    "/bot": {
      "get": {
        "operationId": "getBotPolicy",
        "summary": "Get the AgentVivaBot policy page",
        "description": "No API key needed. Explains what AgentVivaBot is and how a site can opt out of being probed.",
        "tags": [
          "Public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The policy page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "operationId": "getHealth",
        "summary": "Health check",
        "description": "No API key needed. Always `ok` when the Worker is up.",
        "tags": [
          "Public"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "ok"
                  ]
                },
                "example": "ok"
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "av_live_...",
        "description": "A team's API key, issued once at creation and shown only then. Send it as `Authorization: Bearer av_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`. A missing or unknown key is `401 unauthorized`.\n"
      }
    },
    "parameters": {
      "MonitorId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "The monitor's `id`."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "`error` is one of: `invalid_json` (the request body is not valid JSON), `invalid_monitor` (a monitor field failed validation), `invalid_target` (a disallowed scheme or private host in a monitor's `target.url`, or in `PATCH /v1/team`'s `webhook_url` -- the same rule applies to both), `invalid_query` (a malformed query string, or a request body that fails validation when no more specific code applies, e.g. `PATCH /v1/team`), `domain_not_claimable` (`POST /v1/claims` on a monitor, or `GET /v1/claimable` for a domain, that is a shared or public suffix nobody can prove they own), `webhook_test_failed` (`PATCH /v1/team` set `webhook_url`, or `POST /v1/team/webhook/test` was called, and the one test delivery did not answer 2xx within 5 s), or `catalog_unreadable` (`PUT /v1/catalog`: the link could not be fetched, is not a JSON or CSV list with an id and a URL per tool, or holds no valid tool).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`error` is `unauthorized`: the `Authorization` header is missing an `av_live_...` key, or the key is unknown.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "unauthorized"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "`error` is `plan_limit` (the team's monitor limit is reached) or `plan_required` (the request needs a plan this team does not have -- today, the only case is a canary check on the `free` plan).\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "`error` is `not_found`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "not_found"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "`error` is `body_too_large`: the request body is larger than 64 KB.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "body_too_large"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "`error` is `rate_limited`: more than 600 requests in the last minute for this API key. A bulk call counts as one request.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "rate_limited"
            }
          }
        }
      },
      "InternalError": {
        "description": "`error` is `internal_error`: an unexpected server error, logged our side, safe to retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "ok": false,
              "error": "internal_error"
            }
          }
        }
      }
    },
    "schemas": {
      "CheckType": {
        "type": "string",
        "enum": [
          "page",
          "json",
          "canary",
          "mcp"
        ],
        "description": "The kind of check a monitor runs. `mcp` checks a hosted MCP server the way an AI app connects to it.\n"
      },
      "MonitorState": {
        "type": "string",
        "enum": [
          "unknown",
          "up",
          "degraded",
          "down",
          "blocked"
        ],
        "description": "A monitor's current status. `unknown` is the state of every monitor before its first confirmed probe. `blocked` means the target refused the probe (401, 403, 429, 400, 405, 406, 451, a bot challenge), that an MCP server asks AI apps to sign in (`auth_required`), or that our network reaches a different server than the public does (`route_mismatch`), and is never treated as dead -- it never becomes `down`. A state changes only after `confirmations` consecutive probes agree, re-probed ten minutes apart (an hour apart after the second for monitors checked hourly or less often). After `down` or `degraded`, the monitor is probed again after 1, 4 and 12 hours, then at its interval.\n"
      },
      "Verdict": {
        "type": "string",
        "enum": [
          "up",
          "degraded",
          "down",
          "blocked"
        ],
        "description": "The outcome of one probe. Unlike `MonitorState`, a single probe's verdict is never `unknown` -- `unknown` describes a monitor that has not yet completed enough confirming probes, not the outcome of any one probe (`src/checks/types.ts`'s `Verdict` type excludes it).\n"
      },
      "Reason": {
        "enum": [
          "net_error",
          "timeout",
          "http_404",
          "http_5xx",
          "http_other",
          "challenge",
          "refused",
          "robots",
          "route_mismatch",
          "parked",
          "redirect_offsite",
          "moved",
          "redirect_loop",
          "redirect_refused",
          "rule_failed",
          "latency",
          "invalid_json",
          "body_truncated",
          "judge_failed",
          "judge_unavailable",
          "alarm_retry_exhausted",
          "auth_required",
          "mcp_invalid",
          "mcp_error",
          "mcp_version",
          "mcp_tools_failed",
          "mcp_tool_missing",
          null
        ],
        "description": "Why a probe produced the state it did; `null` on a clean, unremarkable `up`. `moved` is the one reason an `up` probe carries: the address redirects to a domain with the same name (`v0.dev` to `v0.app`) and that page works; `evidence.moved_to` holds the new origin. `redirect_offsite` (`degraded`) is a redirect to a site with a different name. `robots` (`blocked`): the site's `robots.txt` asks AgentVivaBot (or `*`) not to open a `page` target. The probe reason `invalid_json` (a `json`/`canary` target's body did not parse) is distinct from the `400 invalid_json` request-validation error -- that one is about a malformed API request, never a probe result. `body_truncated` means a `json`/`canary` response was larger than the read cap and got cut off before it could be parsed; the verdict is `up`, not `degraded`, because this is AgentViva's own limitation, not evidence against the target. `alarm_retry_exhausted` is attached to a probe row that never carries a completed check result and never feeds a state change. The `mcp` check adds `auth_required` (verdict `blocked`: the server answered `401` and asks for sign-in; put a key in `target.headers` to check it fully), `mcp_invalid` (a 2xx that is not a valid JSON-RPC answer, a redirect that turns `POST` into `GET`, or a `405` to `initialize`), `mcp_error` (a JSON-RPC error to `initialize`), `mcp_version` (a modern server that lists no protocol version AgentViva speaks), `mcp_tools_failed` (`tools/list` failed) and `mcp_tool_missing` (a tool in `expect.mcp.tools` is not listed); the last four give the verdict `degraded`. `route_mismatch` also applies to an `mcp` target, and `body_truncated` also applies to an MCP reply that was larger than the read cap.\n"
      },
      "Target": {
        "type": "object",
        "description": "What a monitor probes. On a monitor read back from the API, `body` is always a string or `null`: an object body sent on create/patch is JSON-serialized before storage.\n",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "`http` or `https` only, and a public host: private address ranges (`10/8`, `172.16/12`, `192.168/16`, `127/8`, link-local, `localhost`) are rejected with `400 invalid_target`.\n",
            "example": "https://www.notion.so"
          },
          "method": {
            "type": "string",
            "enum": [
              "GET",
              "POST",
              "PUT",
              "HEAD"
            ],
            "description": "Defaults to `GET` for `page` and `json` monitors, `POST` for `canary` and `mcp` monitors. An `mcp` monitor takes only `POST`.",
            "example": "GET"
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 1024
            },
            "maxProperties": 10,
            "default": {},
            "description": "At most 10 entries, each value at most 1 KB. Stored encrypted at rest **v1**, plain in v0.",
            "example": {}
          },
          "body": {
            "description": "At most 16 KB. A string or a JSON object when creating/patching a monitor; always a string or `null` once read back (see above). Must be left out for `mcp` (the check writes its own messages).\n",
            "oneOf": [
              {
                "type": "string",
                "maxLength": 16384
              },
              {
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "default": null,
            "example": null
          }
        }
      },
      "ExpectRule": {
        "type": "object",
        "description": "One rule checked against a `json`/`canary` monitor's parsed response body. `mcp` monitors take no rules.",
        "required": [
          "path",
          "op"
        ],
        "properties": {
          "path": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "A path into the parsed JSON response.",
            "example": "choices[0].message.content"
          },
          "op": {
            "type": "string",
            "enum": [
              "exists",
              "eq",
              "contains",
              "regex",
              "gte",
              "lte"
            ]
          },
          "value": {
            "description": "The comparison value, required for every `op` except `exists`. Any JSON type."
          }
        }
      },
      "Canary": {
        "type": "object",
        "description": "Required (and only meaningful) when a monitor's `type` is `canary`. The literal string `{{question}}` is substituted into `target.body` wherever it appears.\n",
        "required": [
          "question",
          "expectation"
        ],
        "properties": {
          "question": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          },
          "expectation": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          },
          "reply_path": {
            "type": "string",
            "maxLength": 200,
            "default": "",
            "description": "Path into the JSON reply body to check against `expectation`. The empty string (the default) means \"use the whole reply body\".\n"
          }
        }
      },
      "ExpectMcp": {
        "type": "object",
        "description": "What an `mcp` monitor requires from the MCP server.",
        "properties": {
          "tools": {
            "type": "array",
            "maxItems": 50,
            "default": [],
            "description": "Tool names the server must list. When AgentViva's paging limits cut the tool list (10 pages, or a `nextCursor` longer than 4,096 characters), a required name it did not see is not called missing: the probe stays `up`, with `tools_truncated: true` and the names in `unchecked` in its evidence.\n",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128,
              "pattern": "^[A-Za-z0-9_./-]+$"
            },
            "example": [
              "search",
              "fetch"
            ]
          }
        }
      },
      "Expect": {
        "type": "object",
        "description": "What counts as a passing probe.",
        "properties": {
          "status": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 100,
              "maximum": 599
            },
            "minItems": 2,
            "maxItems": 2,
            "default": [
              200,
              299
            ],
            "description": "`[min, max]`, inclusive HTTP status range. Not used by `mcp` (the protocol fixes the statuses).",
            "example": [
              200,
              299
            ]
          },
          "max_ms": {
            "type": "integer",
            "minimum": 100,
            "maximum": 10000,
            "default": 10000,
            "description": "Bounds the HTTP fetch itself. For `page`/`json` checks that fetch is also the whole check, so 10 s is the worst-case total. A `canary` check adds a second, independent call to the judge model afterward (its own fixed ~8 s budget, separate from `max_ms`), so a canary's worst-case total latency is closer to 18 s; a probe result's `latency_ms` reflects only the first (fetch) phase. For `mcp`, it is compared with the one request that gave the good answer (`server/discover` or `initialize`), and the whole check has a 15 s budget.\n"
          },
          "rules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ExpectRule"
            },
            "maxItems": 10,
            "default": [],
            "description": "Must be empty for `mcp`."
          },
          "canary": {
            "description": "Required when the monitor's `type` is `canary`; ignored otherwise. Must be `null` for `mcp`.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/Canary"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "mcp": {
            "description": "For `mcp` monitors only; left out means `{ \"tools\": [] }`. Refused on the other types (`null` is accepted there and is not stored). Dropped when a monitor's `type` is changed away from `mcp`.\n",
            "oneOf": [
              {
                "$ref": "#/components/schemas/ExpectMcp"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "MonitorCreate": {
        "type": "object",
        "description": "Fields you send to create a monitor, or replace on `PATCH` (see `MonitorPatch`). For `type: mcp`: `target.method` is `POST` only (the default), `target.body` and `expect.rules` are left out, `expect.canary` is `null`, and `expect.status` is not used.\n",
        "required": [
          "type",
          "name",
          "target"
        ],
        "properties": {
          "type": {
            "$ref": "#/components/schemas/CheckType"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "example": "Notion"
          },
          "external_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "Unique within your team. The upsert key for `POST /v1/monitors/bulk`.",
            "example": "notion"
          },
          "target": {
            "$ref": "#/components/schemas/Target"
          },
          "expect": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Expect"
              }
            ],
            "default": {}
          },
          "interval_sec": {
            "type": "integer",
            "minimum": 300,
            "description": "Seconds between scheduled probes. `page` monitors require `>= 3600` (default 86400); `json` and `canary` monitors require `>= 300` (default 21600); `mcp` monitors require `>= 300` (default 3600). A value below the type's own minimum is `400 invalid_monitor`.\n",
            "example": 86400
          },
          "confirmations": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5,
            "description": "Consecutive agreeing probes, re-probed ten minutes apart (an hour apart after the second for monitors checked hourly or less often), needed before a state change commits. Defaults to 3 for `page` monitors, 2 for `json`, `canary` and `mcp` monitors.\n",
            "example": 3
          },
          "public": {
            "type": "boolean",
            "default": false
          },
          "public_slug": {
            "type": "string",
            "pattern": "^[a-z0-9-]{1,64}$",
            "description": "Required when `public` is `true`. Unique among your team's currently *public* monitors -- a private monitor's `public_slug` is not checked for collisions and can be reused by another monitor.\n",
            "example": "notion"
          },
          "tags": {
            "$ref": "#/components/schemas/TagsInput"
          }
        },
        "example": {
          "type": "page",
          "name": "Notion",
          "external_id": "notion",
          "target": {
            "url": "https://www.notion.so"
          },
          "public": true,
          "public_slug": "notion"
        }
      },
      "TagsInput": {
        "type": "array",
        "maxItems": 10,
        "description": "Up to 10 tags, each 1-32 of `a-z 0-9 - _`. Capitals are lowered, spaces around a tag trimmed and repeats dropped. `[]` clears them. A patch without `tags` keeps them; a bulk upsert without `tags` clears them. For a catalog monitor the catalog list sets them.\n",
        "items": {
          "type": "string",
          "minLength": 1,
          "maxLength": 32
        },
        "example": [
          "featured"
        ]
      },
      "MonitorPatch": {
        "type": "object",
        "description": "Any subset of the `MonitorCreate` fields, plus `paused`. Changing `interval_sec` or `target` reschedules the next probe.",
        "properties": {
          "type": {
            "$ref": "#/components/schemas/CheckType"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "external_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "target": {
            "$ref": "#/components/schemas/Target"
          },
          "expect": {
            "$ref": "#/components/schemas/Expect"
          },
          "interval_sec": {
            "type": "integer",
            "minimum": 300
          },
          "confirmations": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "public": {
            "type": "boolean"
          },
          "public_slug": {
            "type": "string",
            "pattern": "^[a-z0-9-]{1,64}$"
          },
          "paused": {
            "type": "boolean"
          },
          "tags": {
            "$ref": "#/components/schemas/TagsInput"
          }
        },
        "example": {
          "interval_sec": 43200
        }
      },
      "Monitor": {
        "type": "object",
        "description": "A monitor as returned by the API. Every field beyond the `MonitorCreate` fields is set by the server.",
        "required": [
          "id",
          "external_id",
          "type",
          "name",
          "target",
          "expect",
          "interval_sec",
          "confirmations",
          "public",
          "public_slug",
          "paused",
          "state",
          "state_since",
          "last_probe_at",
          "last_ok_at",
          "last_latency_ms",
          "last_reason",
          "source",
          "tags",
          "decision",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456"
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "example": "notion"
          },
          "type": {
            "$ref": "#/components/schemas/CheckType"
          },
          "name": {
            "type": "string",
            "example": "Notion"
          },
          "target": {
            "$ref": "#/components/schemas/Target"
          },
          "expect": {
            "$ref": "#/components/schemas/Expect"
          },
          "interval_sec": {
            "type": "integer",
            "example": 86400
          },
          "confirmations": {
            "type": "integer",
            "example": 3
          },
          "public": {
            "type": "boolean",
            "example": true
          },
          "public_slug": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[a-z0-9-]{1,64}$",
            "example": "notion"
          },
          "paused": {
            "type": "boolean",
            "example": false
          },
          "state": {
            "$ref": "#/components/schemas/MonitorState"
          },
          "state_since": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Unix milliseconds since `state` last changed.",
            "example": 1789200000000
          },
          "last_probe_at": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "example": 1789286400000
          },
          "last_ok_at": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "example": 1789286400000
          },
          "last_latency_ms": {
            "type": [
              "integer",
              "null"
            ],
            "example": 412
          },
          "last_reason": {
            "$ref": "#/components/schemas/Reason"
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "catalog",
              null
            ],
            "description": "`catalog` when your catalog owns the monitor, otherwise null."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The monitor's tags; empty when there are none.",
            "example": [
              "featured"
            ]
          },
          "decision": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Decision"
              },
              {
                "type": "null"
              }
            ],
            "description": "Your answer on `GET /v1/decisions` while it still counts; otherwise null."
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "example": 1789100000000
          },
          "updated_at": {
            "type": "integer",
            "format": "int64",
            "example": 1789286400000
          }
        }
      },
      "Decision": {
        "type": "object",
        "description": "Your answer for a monitor on the \"Needs a decision\" list.",
        "required": [
          "action",
          "at",
          "until"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "keep",
              "false_alarm",
              "remove"
            ]
          },
          "at": {
            "type": "integer",
            "format": "int64",
            "description": "Unix milliseconds when you answered.",
            "example": 1793600000000
          },
          "until": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Unix milliseconds when a `keep` or `false_alarm` ends; null for `remove`.",
            "example": 1794800000000
          }
        }
      },
      "MonitorActionRequest": {
        "type": "object",
        "required": [
          "action",
          "ids"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "pause",
              "resume",
              "delete"
            ]
          },
          "ids": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": {
              "type": "string"
            }
          }
        },
        "example": {
          "action": "pause",
          "ids": [
            "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456"
          ]
        }
      },
      "MonitorActionResult": {
        "type": "object",
        "required": [
          "id",
          "outcome"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "paused",
              "resumed",
              "deleted",
              "error"
            ]
          },
          "error": {
            "type": "string",
            "enum": [
              "not_found",
              "internal_error"
            ]
          },
          "detail": {
            "type": "string"
          }
        },
        "example": {
          "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
          "outcome": "paused"
        }
      },
      "DecisionRequest": {
        "type": "object",
        "required": [
          "action",
          "ids"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "keep",
              "false_alarm",
              "stop",
              "remove"
            ]
          },
          "ids": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": {
              "type": "string"
            }
          },
          "days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 365,
            "description": "For `keep` and `false_alarm`: how many days the tool stays off the list. Default is your team's `decide_after_days`.\n"
          }
        },
        "example": {
          "action": "keep",
          "ids": [
            "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456"
          ],
          "days": 14
        }
      },
      "DecisionResult": {
        "type": "object",
        "required": [
          "id",
          "outcome"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "kept",
              "false_alarm",
              "stopped",
              "remove_requested",
              "error"
            ]
          },
          "error": {
            "type": "string",
            "enum": [
              "not_found",
              "not_failing",
              "internal_error"
            ],
            "description": "`not_found`: not one of your monitors. `not_failing`: `remove` for a tool that is not on the list's rule (only a `down` tool, or a `degraded` one with reason `parked`, can be sent for removal).\n"
          },
          "detail": {
            "type": "string"
          },
          "until": {
            "type": "integer",
            "format": "int64",
            "description": "With `kept` and `false_alarm`. Unix milliseconds when the answer ends."
          },
          "requested_at": {
            "type": "integer",
            "format": "int64",
            "description": "With `remove_requested`. Unix milliseconds of the request."
          },
          "webhook": {
            "type": "string",
            "enum": [
              "queued",
              "not_configured",
              "already_sent"
            ],
            "description": "With `remove_requested`. `queued`: the delivery runs right after the answer (also for a request recorded while no webhook was set, once one is set). `not_configured`: no webhook is set, the request waits in `status=remove_requested`. `already_sent`: the tool was already sent for removal in this state, nothing new is sent.\n"
          }
        },
        "example": {
          "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
          "outcome": "remove_requested",
          "requested_at": 1793600000000,
          "webhook": "queued"
        }
      },
      "RemoveRequest": {
        "type": "object",
        "description": "The body of the signed `remove_requested` webhook that `POST /v1/decisions` with `remove` sends to your `webhook_url`, with the same headers and signature as every webhook. One delivery per tool. `id` is the same on every try, so you can ignore a repeat.\n",
        "required": [
          "event",
          "id",
          "monitor_id",
          "external_id",
          "name",
          "url",
          "state",
          "state_since",
          "reason",
          "requested_at",
          "status_url"
        ],
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "remove_requested"
            ]
          },
          "id": {
            "type": "string",
            "example": "rm_0b2f1a2e-4c3d-4b8a-9e21-abcdef123456_1793600000000"
          },
          "monitor_id": {
            "type": "string",
            "format": "uuid"
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "example": "notion"
          },
          "name": {
            "type": "string",
            "example": "Notion"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "example": "https://www.notion.so"
          },
          "state": {
            "$ref": "#/components/schemas/MonitorState"
          },
          "state_since": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "example": 1792900000000
          },
          "reason": {
            "$ref": "#/components/schemas/Reason"
          },
          "requested_at": {
            "type": "integer",
            "format": "int64",
            "example": 1793600000000
          },
          "status_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "example": "https://agentviva.dev/s/acme/notion"
          }
        }
      },
      "DailyResult": {
        "type": "object",
        "description": "The worst result of one UTC day on which a monitor was checked.",
        "required": [
          "date",
          "worst"
        ],
        "properties": {
          "date": {
            "type": "string",
            "format": "date",
            "example": "2026-10-17"
          },
          "worst": {
            "type": "string",
            "enum": [
              "up",
              "blocked",
              "degraded",
              "down"
            ],
            "description": "The worst result of that day, in the order `down`, `degraded`, `blocked`, `up`.",
            "example": "down"
          }
        }
      },
      "DailyResults": {
        "type": "array",
        "description": "One entry per UTC day with a check, oldest first. A day with no check is not in the list. Results are kept 30 days, so 30 days is the most.\n",
        "items": {
          "$ref": "#/components/schemas/DailyResult"
        },
        "example": [
          {
            "date": "2026-10-16",
            "worst": "up"
          },
          {
            "date": "2026-10-17",
            "worst": "down"
          }
        ]
      },
      "ProbeResult": {
        "type": "object",
        "description": "The outcome of one probe run right now, as returned by `POST /v1/monitors/{id}/probe`.",
        "required": [
          "verdict",
          "http_status",
          "latency_ms",
          "reason",
          "evidence"
        ],
        "properties": {
          "verdict": {
            "$ref": "#/components/schemas/Verdict"
          },
          "http_status": {
            "type": [
              "integer",
              "null"
            ]
          },
          "latency_ms": {
            "type": "integer"
          },
          "reason": {
            "$ref": "#/components/schemas/Reason"
          },
          "evidence": {
            "$ref": "#/components/schemas/Evidence"
          }
        }
      },
      "Evidence": {
        "type": "object",
        "description": "Free-form detail about a probe's outcome; which fields are present varies by check type and reason (`src/checks/types.ts`'s `ProbeResult.evidence` is an untyped record). Common fields are documented below; the object is not closed.\n",
        "properties": {
          "http_status": {
            "type": [
              "integer",
              "null"
            ]
          },
          "final_url": {
            "type": "string",
            "description": "The URL the probe actually landed on after following redirects.",
            "example": "https://www.notion.so/"
          },
          "hops": {
            "type": "integer",
            "description": "Number of redirect hops followed."
          },
          "snippet": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "A whitespace-collapsed excerpt of the response body."
          },
          "body": {
            "description": "The parsed or truncated response body captured for a `json`/`canary` check."
          },
          "rule": {
            "description": "The `ExpectRule` that failed, capped for storage."
          },
          "reply": {
            "type": "string",
            "description": "The canary judge's extracted reply text."
          },
          "judge_note": {
            "type": [
              "string",
              "null"
            ],
            "description": "The judge model's explanation."
          },
          "confidence": {
            "type": "number",
            "description": "The judge model's confidence, 0-1."
          },
          "error": {
            "type": "string",
            "description": "A truncated error message for a `net_error` or other internal failure."
          },
          "refused": {
            "type": "string",
            "description": "Which redirect-refusal rule fired (offsite or loop)."
          },
          "matched": {
            "type": "string",
            "description": "The parked-domain signature that matched."
          },
          "body_cap": {
            "type": "integer",
            "description": "The byte cap applied when a response body was truncated."
          },
          "latency_ms": {
            "type": "integer",
            "description": "On a `latency` reason, the observed latency that exceeded `expect.max_ms`."
          },
          "check": {
            "type": "string",
            "enum": [
              "mcp"
            ],
            "description": "Always `mcp` in the evidence of an `mcp` check."
          },
          "era": {
            "type": "string",
            "enum": [
              "modern",
              "legacy"
            ],
            "description": "`mcp` check: `modern` when the server answered `server/discover` with protocol `2026-07-28`, otherwise `legacy` (the `initialize` handshake)."
          },
          "protocol_version": {
            "type": "string",
            "description": "`mcp` check: the MCP protocol version used."
          },
          "server": {
            "type": "object",
            "description": "`mcp` check: the server's own name and version.",
            "properties": {
              "name": {
                "type": "string",
                "maxLength": 100
              },
              "version": {
                "type": "string",
                "maxLength": 100
              }
            }
          },
          "tool_count": {
            "type": "integer",
            "description": "`mcp` check: how many tools the server listed."
          },
          "tools": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "string",
              "maxLength": 128
            },
            "description": "`mcp` check: the first 50 tool names."
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "`mcp` check: required tool names that were not found."
          },
          "tools_truncated": {
            "type": "boolean",
            "description": "`mcp` check: our paging limits cut the tool list; see `unchecked`."
          },
          "unchecked": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "`mcp` check: required names not seen in a cut list; not called missing."
          },
          "session": {
            "type": "boolean",
            "description": "`mcp` check: `true` when the server gave a session id (the id itself is never stored)."
          },
          "redirect": {
            "type": "integer",
            "description": "`mcp` check: the status of a redirect that turns `POST` into `GET`."
          },
          "hint": {
            "type": "string",
            "description": "`mcp` check: for example `old_sse_transport` on a `405` when the path ends in `/sse`."
          },
          "refused_url": {
            "type": "string",
            "maxLength": 500,
            "description": "`mcp` check, `redirect_refused`: the refused address."
          },
          "supported": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "string"
            },
            "description": "`mcp` check, `mcp_version`: the protocol versions the server named."
          },
          "auth": {
            "type": "string",
            "enum": [
              "oauth"
            ],
            "description": "`mcp` check, `auth_required`: present when the server's `401` points to OAuth protected-resource metadata (RFC 9728) that lists authorization servers."
          },
          "authorization_servers": {
            "type": "array",
            "maxItems": 3,
            "items": {
              "type": "string",
              "maxLength": 200
            },
            "description": "`mcp` check, `auth_required`: the first 3 authorization servers."
          }
        },
        "additionalProperties": true,
        "example": {
          "http_status": 404,
          "final_url": "https://www.notion.so/",
          "snippet": null,
          "judge_note": null
        }
      },
      "StateChange": {
        "type": "object",
        "description": "One state change. The same object is the body of the state-change webhook. A remove request in your webhook has `\"event\": \"remove_requested\"` and its own body (see `RemoveRequest`).\n",
        "required": [
          "event",
          "id",
          "monitor_id",
          "external_id",
          "name",
          "from",
          "to",
          "at",
          "reason",
          "evidence",
          "checks",
          "first_seen_at",
          "url",
          "status_url"
        ],
        "properties": {
          "event": {
            "type": "string",
            "enum": [
              "state_change"
            ],
            "description": "Always `state_change` here; it tells a change apart from a remove request in your webhook."
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "monitor_id": {
            "type": "string",
            "format": "uuid"
          },
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "example": "notion"
          },
          "name": {
            "type": "string",
            "example": "Notion"
          },
          "from": {
            "$ref": "#/components/schemas/MonitorState"
          },
          "to": {
            "$ref": "#/components/schemas/MonitorState"
          },
          "at": {
            "type": "integer",
            "format": "int64",
            "example": 1789286400000
          },
          "reason": {
            "$ref": "#/components/schemas/Reason"
          },
          "evidence": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Evidence"
              },
              {
                "type": "null"
              }
            ]
          },
          "checks": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How many agreeing checks committed the change; `null` for changes made before 2026-11-03.",
            "example": 3
          },
          "first_seen_at": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Unix milliseconds when the first of those checks ran; `null` for changes made before 2026-11-03.",
            "example": 1789282200000
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The address the monitor checks (`target.url`); `null` when the monitor has been deleted.",
            "example": "https://www.notion.so/"
          },
          "status_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Present only when the monitor is public; `null` otherwise.",
            "example": "https://agentviva.dev/s/aigregator/notion"
          }
        }
      },
      "Team": {
        "type": "object",
        "required": [
          "name",
          "slug",
          "plan",
          "monitor_limit",
          "monitors_by_state",
          "telegram_linked",
          "telegram_digest",
          "webhook_configured",
          "decide_after_days",
          "instant_tags"
        ],
        "properties": {
          "name": {
            "type": "string",
            "example": "aigregator"
          },
          "slug": {
            "type": "string",
            "example": "aigregator"
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "pro"
            ]
          },
          "monitor_limit": {
            "type": "integer",
            "description": "Set per team, not by plan -- this is the team's own value.",
            "example": 5000
          },
          "monitors_by_state": {
            "type": "object",
            "description": "Count of this team's monitors per state. A state with zero monitors is omitted entirely, not present with a value of `0`.\n",
            "properties": {
              "unknown": {
                "type": "integer"
              },
              "up": {
                "type": "integer"
              },
              "degraded": {
                "type": "integer"
              },
              "down": {
                "type": "integer"
              },
              "blocked": {
                "type": "integer"
              }
            },
            "additionalProperties": false,
            "example": {
              "up": 412,
              "down": 3,
              "degraded": 1
            }
          },
          "telegram_linked": {
            "type": "boolean"
          },
          "telegram_digest": {
            "type": "boolean",
            "description": "`true`: Telegram gets one summary a day (07:00 UTC) instead of a message per change. Webhooks are not affected.\n"
          },
          "webhook_configured": {
            "type": "boolean"
          },
          "decide_after_days": {
            "type": "integer",
            "description": "How many days a tool must not work before it shows on `GET /v1/decisions`. Default 7.",
            "example": 7
          },
          "instant_tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "With the daily summary on, tools with one of these tags still get one Telegram message per change.",
            "example": []
          }
        }
      },
      "Alert": {
        "type": "object",
        "description": "One alert delivery to one channel for one state change.",
        "required": [
          "id",
          "monitor_id",
          "name",
          "own",
          "change_id",
          "from",
          "to",
          "channel",
          "sent",
          "attempts",
          "error",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "monitor_id": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "`null` when the monitor has been deleted.",
            "example": "Jellypod"
          },
          "own": {
            "type": "boolean",
            "description": "`false` for a monitor your team only has a claim on, or a deleted one."
          },
          "change_id": {
            "type": "string"
          },
          "from": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/MonitorState"
              },
              {
                "type": "null"
              }
            ]
          },
          "to": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/MonitorState"
              },
              {
                "type": "null"
              }
            ]
          },
          "channel": {
            "type": "string",
            "enum": [
              "webhook",
              "telegram",
              "action"
            ],
            "description": "`action` is a remove request sent to your webhook (see `POST /v1/decisions`); its `from` and `to` are `null`."
          },
          "sent": {
            "type": "boolean",
            "description": "Whether the last try arrived."
          },
          "attempts": {
            "type": "integer",
            "description": "0 means not tried yet.",
            "example": 1
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the last try did not arrive; `null` otherwise.",
            "example": "webhook 500"
          },
          "created_at": {
            "type": "integer",
            "format": "int64",
            "example": 1792238900000
          }
        }
      },
      "TeamPatch": {
        "type": "object",
        "description": "Set or clear a team's notification channels and settings. Every field is optional; one you leave out stays as it is. The two channel fields are independently nullable.\n",
        "properties": {
          "webhook_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "maxLength": 2048,
            "description": "`http` or `https`, and a public host -- the same rule a monitor's `target.url` follows. `https` is recommended: the payload is signed, not encrypted.\n",
            "example": "https://hooks.aigregator.com/agentviva"
          },
          "telegram_chat_id": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64,
            "example": "123456789"
          },
          "telegram_digest": {
            "type": "boolean",
            "description": "`true`: Telegram gets no message per change; at 07:00 UTC it gets one summary of the last 24 hours (changes to `blocked` and `unknown` to `up` are left out). Webhooks are not affected.\n"
          },
          "decide_after_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 90,
            "description": "How many days a tool must not work before it shows on `GET /v1/decisions`. Default 7.\n"
          },
          "instant_tags": {
            "description": "The same rules as a monitor's `tags`; `null` or `[]` clears them. Tools with one of these tags still get one Telegram message per change when the daily summary is on.\n",
            "oneOf": [
              {
                "$ref": "#/components/schemas/TagsInput"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "Claim": {
        "type": "object",
        "required": [
          "id",
          "monitor_id",
          "domain",
          "token",
          "verified",
          "method",
          "verified_at",
          "dns",
          "file"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "monitor_id": {
            "type": "string",
            "format": "uuid"
          },
          "domain": {
            "type": "string",
            "description": "The registrable domain proof was (or must be) demonstrated for.",
            "example": "notion.so"
          },
          "token": {
            "type": "string",
            "example": "av-verify-9f8e7d6c5b4a3928"
          },
          "verified": {
            "type": "boolean"
          },
          "method": {
            "enum": [
              "dns",
              "file",
              null
            ],
            "description": "Which method verified the claim; `null` until verified."
          },
          "verified_at": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64"
          },
          "dns": {
            "type": "object",
            "description": "The DNS TXT record that proves ownership.",
            "required": [
              "name",
              "type",
              "value"
            ],
            "properties": {
              "name": {
                "type": "string",
                "example": "_agentviva.notion.so"
              },
              "type": {
                "type": "string",
                "enum": [
                  "TXT"
                ]
              },
              "value": {
                "type": "string"
              }
            }
          },
          "file": {
            "type": "object",
            "description": "The well-known file that proves ownership, as an alternative to DNS.",
            "required": [
              "path",
              "body"
            ],
            "properties": {
              "path": {
                "type": "string",
                "enum": [
                  "/.well-known/agentviva-challenge"
                ]
              },
              "body": {
                "type": "string"
              }
            }
          }
        }
      },
      "BulkRequest": {
        "type": "object",
        "required": [
          "monitors"
        ],
        "properties": {
          "monitors": {
            "type": "array",
            "minItems": 1,
            "maxItems": 1000,
            "items": {
              "$ref": "#/components/schemas/MonitorCreate"
            },
            "description": "Up to 1,000 items, each validated and processed independently -- one bad item does not fail the batch. An item whose `external_id` matches an existing monitor on your team updates it (fields you send replace the stored ones; state and history are kept); otherwise it is created.\n"
          }
        },
        "example": {
          "monitors": [
            {
              "external_id": "notion",
              "type": "page",
              "name": "Notion",
              "target": {
                "url": "https://www.notion.so"
              },
              "public": true,
              "public_slug": "notion"
            }
          ]
        }
      },
      "BulkResult": {
        "type": "object",
        "required": [
          "external_id",
          "id",
          "outcome"
        ],
        "properties": {
          "external_id": {
            "type": [
              "string",
              "null"
            ],
            "example": "notion"
          },
          "id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "outcome": {
            "type": "string",
            "enum": [
              "created",
              "updated",
              "error"
            ]
          },
          "error": {
            "type": "string",
            "enum": [
              "invalid_monitor",
              "invalid_target",
              "plan_required",
              "plan_limit",
              "internal_error"
            ]
          },
          "detail": {
            "type": "string"
          }
        },
        "example": {
          "external_id": "notion",
          "id": "0b2f1a2e-4c3d-4b8a-9e21-abcdef123456",
          "outcome": "created"
        }
      },
      "PublicStatus": {
        "type": "object",
        "required": [
          "state",
          "since",
          "last_probe_at",
          "latency_ms",
          "reason",
          "evidence"
        ],
        "properties": {
          "state": {
            "$ref": "#/components/schemas/MonitorState"
          },
          "since": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "example": 1789200000000
          },
          "last_probe_at": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "example": 1789286400000
          },
          "latency_ms": {
            "type": [
              "integer",
              "null"
            ],
            "example": 412
          },
          "reason": {
            "$ref": "#/components/schemas/Reason"
          },
          "evidence": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PublicEvidence"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "example": {
          "state": "up",
          "since": 1789200000000,
          "last_probe_at": 1789286400000,
          "latency_ms": 412,
          "reason": null,
          "evidence": {
            "http_status": 200,
            "latency_ms": 412,
            "title": "Notion"
          }
        }
      },
      "InstantCheck": {
        "type": "object",
        "required": [
          "ok",
          "url",
          "type",
          "checked_at",
          "state",
          "reason",
          "reason_text",
          "meaning",
          "evidence",
          "watched",
          "cached"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "url": {
            "type": "string",
            "description": "The address as checked."
          },
          "type": {
            "type": "string",
            "enum": [
              "page",
              "mcp"
            ]
          },
          "checked_at": {
            "type": "integer",
            "format": "int64"
          },
          "state": {
            "type": "string",
            "enum": [
              "up",
              "degraded",
              "down",
              "blocked"
            ],
            "description": "One check's verdict",
            "not a confirmed state.": null
          },
          "reason": {
            "$ref": "#/components/schemas/Reason"
          },
          "reason_text": {
            "type": "string"
          },
          "meaning": {
            "type": "string",
            "description": "The verdict in plain words."
          },
          "evidence": {
            "$ref": "#/components/schemas/PublicEvidence"
          },
          "watched": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Public monitors on the same domain (the `GET /v1/public/search` shape)."
          },
          "cached": {
            "type": "boolean"
          }
        }
      },
      "PublicCheckError": {
        "type": "object",
        "required": [
          "ok",
          "error",
          "detail"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "string",
            "enum": [
              "invalid_url",
              "invalid_query",
              "invalid_json",
              "rate_limited"
            ]
          },
          "detail": {
            "type": "string"
          }
        }
      },
      "PublicEvidence": {
        "type": "object",
        "description": "What the newest check saw, cut down to fields that are safe to show anyone. Never a path or a query string of an address, a header, a canary question, expectation or reply, a JSON body or a rule. A field with nothing to say is left out; the whole object is at most 1 KB.\n",
        "required": [
          "http_status",
          "latency_ms"
        ],
        "properties": {
          "http_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "`null` when the connection failed or timed out."
          },
          "latency_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "final_host": {
            "type": "string",
            "description": "`page`: the host the redirects ended on, when not the address's host."
          },
          "redirects": {
            "type": "integer",
            "description": "`page`: how many redirects were followed."
          },
          "moved_to": {
            "type": "string",
            "description": "`page`, `json`: the new origin of a tool that moved (reason `moved`)."
          },
          "title": {
            "type": "string",
            "maxLength": 120,
            "description": "`page`: the page's title."
          },
          "tool_count": {
            "type": "integer",
            "description": "`mcp`: how many tools the server lists."
          },
          "tools": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "`mcp`: the first tool names."
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "`mcp`: required tools it does not list."
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "`canary`: how sure the judge was."
          }
        }
      },
      "CatalogItem": {
        "type": "object",
        "required": [
          "id",
          "name",
          "url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "notion"
          },
          "name": {
            "type": "string",
            "example": "Notion"
          },
          "url": {
            "type": "string",
            "example": "https://notion.so"
          },
          "tags": {
            "description": "Optional. A JSON array of strings, or one string split on `,`, `|` or `;`. Capitals are lowered; a tag that is not 1-32 of `a-z 0-9 - _` is left out; at most 10.\n",
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "string"
              }
            ],
            "example": [
              "featured"
            ]
          }
        }
      },
      "Catalog": {
        "type": "object",
        "required": [
          "url",
          "public",
          "last_sync_at",
          "next_sync_at",
          "last_result",
          "monitors_by_state",
          "not_watched",
          "down_30_days"
        ],
        "properties": {
          "url": {
            "type": "string"
          },
          "monitors_by_state": {
            "type": "object",
            "description": "Count of the monitors the catalog owns (`source=catalog`) per state. A state with zero monitors is omitted.\n",
            "additionalProperties": {
              "type": "integer"
            },
            "example": {
              "up": 2814,
              "degraded": 171,
              "down": 355,
              "blocked": 82,
              "unknown": 1635
            }
          },
          "not_watched": {
            "type": "integer",
            "description": "The tools you stopped watching (see `GET /v1/catalog/not-watched`).",
            "example": 0
          },
          "down_30_days": {
            "type": "integer",
            "description": "The catalog's monitors that have been `down` for 30 days or more.",
            "example": 41
          },
          "public": {
            "type": "boolean",
            "description": "Whether the monitors the sync creates are public."
          },
          "last_sync_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix ms of the last finished sync; null before the first."
          },
          "next_sync_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unix ms when the next sync is due."
          },
          "last_result": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CatalogResult"
              },
              {
                "type": "null"
              }
            ],
            "description": "The last sync's result; null before the first sync."
          }
        }
      },
      "CatalogResult": {
        "type": "object",
        "required": [
          "found",
          "invalid",
          "created",
          "updated",
          "unchanged",
          "paused",
          "resumed",
          "partial",
          "remaining",
          "errors"
        ],
        "properties": {
          "found": {
            "type": "integer",
            "description": "Tools with a valid id and URL in the list."
          },
          "invalid": {
            "type": "integer",
            "description": "Rows skipped: no id, no URL, or an id that appeared twice."
          },
          "created": {
            "type": "integer",
            "description": "Monitors added."
          },
          "updated": {
            "type": "integer",
            "description": "Monitors whose name, URL or tags changed."
          },
          "unchanged": {
            "type": "integer",
            "description": "Monitors that already matched the list."
          },
          "paused": {
            "type": "integer",
            "description": "Monitors paused because their tool left the list."
          },
          "resumed": {
            "type": "integer",
            "description": "Paused monitors resumed because their tool came back."
          },
          "not_watched": {
            "type": "integer",
            "description": "Tools in the list you stopped watching: no monitor is made for them."
          },
          "partial": {
            "type": "boolean",
            "description": "True when the run stopped with work left (the batch limit of 1,000 creates and updates, or the 8-minute time limit); the next run, within 10 minutes, continues.\n"
          },
          "remaining": {
            "type": "integer",
            "description": "Creates and updates still waiting. 0 when the time limit stopped a run before its resumes or pauses were done.\n"
          },
          "errors": {
            "type": "array",
            "maxItems": 20,
            "description": "The first 20 tools that could not be created, updated, paused or resumed.",
            "items": {
              "type": "object",
              "required": [
                "external_id",
                "detail"
              ],
              "properties": {
                "external_id": {
                  "type": "string"
                },
                "detail": {
                  "type": "string"
                }
              }
            }
          },
          "fatal": {
            "type": "string",
            "description": "Only when the sync stopped before changing anything; says what to check."
          },
          "fatal_kind": {
            "type": "string",
            "enum": [
              "unreadable",
              "empty",
              "shrank",
              "limit",
              "mass_pause",
              "internal"
            ],
            "description": "Which stop it was. `shrank` and `mass_pause` are the two that `force` lifts."
          },
          "fatal_numbers": {
            "type": "array",
            "minItems": 2,
            "maxItems": 2,
            "items": {
              "type": "integer"
            },
            "description": "With `shrank`: [monitors the catalog has, tools in the list]. With `mass_pause`: [monitors to pause, running catalog monitors].\n"
          }
        },
        "example": {
          "found": 5057,
          "invalid": 0,
          "created": 1000,
          "updated": 0,
          "unchanged": 0,
          "paused": 0,
          "resumed": 0,
          "partial": true,
          "remaining": 4057,
          "errors": []
        }
      },
      "CatalogPut": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048
          },
          "public": {
            "type": "boolean",
            "default": true
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "ok",
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          }
        },
        "example": {
          "ok": false,
          "error": "invalid_target",
          "detail": "private address ranges are not allowed"
        }
      }
    }
  }
}
