{
  "openapi": "3.1.0",
  "info": {
    "title": "Flusterduck API",
    "summary": "Read and write access to UX friction data: confusion scores, issues, alerts, deploys, and account management.",
    "description": "Flusterduck detects UX friction automatically from behavioral signals (rage clicks, dead clicks, form abandonment, navigation loops, and more), computes per-page confusion scores, clusters signals into ranked issues, and verifies whether deploys fixed them. The Read API (`/query/*`) returns scores, issues, alerts, deploys, and raw event data. The Write API (`/manage/*`) creates sites, alert rules, webhooks, and integrations, and triages issues. Every response is wrapped as `{ \"data\": ..., \"error\": null }` on success or `{ \"data\": null, \"error\": { \"code\": ..., \"message\": ..., \"status\": ..., \"details\": ... } }` on failure (details is present only when the error carries extra context).\n\nFlusterduck is a paid, proprietary product (not open source). Plans: Grow $99/mo (50,000 sessions, 1 site), Scale $249/mo (250,000 sessions, 5 sites), Pro $499/mo (1,000,000 sessions, 10 sites), Enterprise (custom). Each organization gets one 3-day self-serve trial. A card is required, you pay $0 today, and billing starts after 3 days unless you cancel.",
    "version": "1.0.0",
    "contact": {
      "name": "Flusterduck",
      "url": "https://flusterduck.com",
      "email": "security@flusterduck.app"
    },
    "termsOfService": "https://flusterduck.com/terms"
  },
  "externalDocs": {
    "description": "Full REST API reference",
    "url": "https://docs.flusterduck.com/rest-api"
  },
  "servers": [
    {
      "url": "https://api.flusterduck.com/v1",
      "description": "Production API (Vercel proxy in front of the Supabase edge functions)"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Read",
      "description": "Read-only endpoints under /query. Require the query:read scope."
    },
    {
      "name": "Write",
      "description": "Mutating endpoints under /manage. Require the manage:write scope or a user JWT."
    }
  ],
  "paths": {
    "/query/scores": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Confusion scores for every tracked page",
        "operationId": "getScores",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Scores for every page on the site, sorted by score descending",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                },
                "example": {
                  "data": {
                    "site": {
                      "site_id": "7f2c9d4a-4b5e-4c3a-9b1d-2e6a4f8c7d5b",
                      "overall_score": 34.2,
                      "overall_trend": "up",
                      "pages": [
                        {
                          "page": "/checkout",
                          "score": 72,
                          "trend": "up",
                          "dominant_signal": "rage_click"
                        }
                      ]
                    }
                  },
                  "error": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/query/page": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Full detail for a single page",
        "operationId": "getPage",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "$ref": "#/components/parameters/Page"
          }
        ],
        "responses": {
          "200": {
            "description": "Score history, top elements, open issues, recent deploys, active alerts for one page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/query/issues": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "List UX issues",
        "operationId": "listIssues",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "triaged",
                "in_progress",
                "verified",
                "resolved",
                "ignored"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "UX issues for the site, optionally filtered by status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                },
                "example": {
                  "data": {
                    "issues": [
                      {
                        "id": "iss_3a7f2c9d4e1b",
                        "title": "Dead clicks on complete purchase button",
                        "page": "/checkout",
                        "element_selector": "button[type='submit']",
                        "type": "dead_click",
                        "severity": 84,
                        "status": "open",
                        "affected_sessions": 47
                      }
                    ]
                  },
                  "error": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/query/issues/{issue_id}": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Full detail on one UX issue",
        "operationId": "getIssue",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "$ref": "#/components/parameters/IssueId"
          }
        ],
        "responses": {
          "200": {
            "description": "Issue detail including evidence, session links, verification history",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/query/elements": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Element-level friction breakdown",
        "operationId": "getElements",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "$ref": "#/components/parameters/PageOptional"
          }
        ],
        "responses": {
          "200": {
            "description": "Top elements by friction signal count, with dominant signal type and recommendation",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/element-heatmap": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Click positions within one element",
        "operationId": "getElementHeatmap",
        "description": "Normalized 0..1 click positions (rx, ry) inside a single element, from rage click, dead click, and disabled element attempt signals. Up to 300 points from the most recent 800 matching signals.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "selector",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The element's CSS selector, as returned by /query/elements"
          }
        ],
        "responses": {
          "200": {
            "description": "Click points for the element",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                },
                "example": {
                  "data": {
                    "page": "/checkout",
                    "selector": "button.submit-order",
                    "count": 2,
                    "points": [
                      {
                        "rx": 0.512,
                        "ry": 0.488,
                        "signal": "rage_click"
                      },
                      {
                        "rx": 0.497,
                        "ry": 0.503,
                        "signal": "dead_click"
                      }
                    ]
                  },
                  "error": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/query/page-heatmap": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Page-level friction click map",
        "operationId": "getPageHeatmap",
        "description": "Normalized click positions for friction signals across a whole page, from the last 30 days (up to 500 points). Each point has viewport-relative vx/vy (0..1), document-space px/py when the SDK provided them (use these over a full-page screenshot), the signal type, and the element selector when known. The optional device filter splits touch layout (mobile, includes tablets) from pointer layout (desktop).",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "device",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "desktop",
                "mobile"
              ]
            },
            "description": "Only include clicks from sessions on this device class. Omit for all devices."
          }
        ],
        "responses": {
          "200": {
            "description": "Click points for the page",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                },
                "example": {
                  "data": {
                    "page": "/checkout",
                    "count": 1,
                    "points": [
                      {
                        "vx": 0.62,
                        "vy": 0.41,
                        "px": 0.62,
                        "py": 0.18,
                        "signal": "rage_click",
                        "el": "button.submit-order"
                      }
                    ]
                  },
                  "error": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/query/page-screenshot": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Screenshot backdrop for a page heatmap",
        "operationId": "getPageScreenshot",
        "description": "The cached full-page screenshot for a tracked page (captured for public pages, or customer-uploaded for authed pages). `screenshot: null` is a normal answer: capture disabled, page unrenderable, or capture limits reached; render without a backdrop in that case.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "name": "device",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "desktop",
                "mobile"
              ],
              "default": "desktop"
            }
          },
          {
            "name": "cached",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "Pass 1 to serve only an already-cached capture and never trigger a new one."
          }
        ],
        "responses": {
          "200": {
            "description": "Screenshot descriptor, or screenshot: null when none is available",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/query/alerts": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "List alerts",
        "operationId": "listAlerts",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "fired",
                "acknowledged",
                "investigating",
                "resolved"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Alerts for the site",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/alerts/{alert_id}": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Full detail on one alert",
        "operationId": "getAlert",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "alert_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Alert detail",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/query/flows": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Page-to-page navigation edges from recent sessions",
        "operationId": "getFlows",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Navigation flow edges",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/trends": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Confusion score history over time",
        "operationId": "getTrends",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "$ref": "#/components/parameters/PageOptional"
          },
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90,
              "default": 7
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Score history",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                },
                "example": {
                  "data": {
                    "page": "/checkout",
                    "days": 14,
                    "history": [
                      {
                        "date": "2026-06-10",
                        "score": 72
                      },
                      {
                        "date": "2026-06-09",
                        "score": 61
                      }
                    ]
                  },
                  "error": null
                }
              }
            }
          }
        }
      }
    },
    "/query/deploys": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "List deploys with before/after confusion scores",
        "operationId": "listDeploys",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deploys with confusion_before, confusion_after (null until 5 minutes of post-deploy data), and related issues",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/deploys/{deploy_id}": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Full detail on one deploy",
        "operationId": "getDeploy",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "deploy_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deploy detail",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/compare": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Side-by-side confusion score comparison between two pages",
        "operationId": "comparePages",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "a",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "First page path"
          },
          {
            "name": "b",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Second page path"
          }
        ],
        "responses": {
          "200": {
            "description": "Comparison result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/recommendations": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Ranked fix recommendations derived from the current issue set",
        "operationId": "getRecommendations",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Prioritized fix list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/revenue": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Revenue at risk across open issues",
        "operationId": "getRevenue",
        "description": "Populated only when conversion tracking (track()) is wired on the site.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Revenue impact estimates",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/insights": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Confused-vs-calm conversion analysis",
        "operationId": "getInsights",
        "description": "How much less confused sessions convert than calm ones, broken down by page and traffic source, with ranked narratable insights. Needs a conversion event wired.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90,
              "default": 7
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Conversion insights",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/explore": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Typed exploratory query over behavioral data (query string form)",
        "operationId": "runExploreGet",
        "description": "Explorer is a deterministic, typed query engine over session/event data: no natural language, no LLM. It takes a time window, up to 12 AND-ed filters over a closed field list (signal, page, source, confused, converted, event_type) with operator is/is_not/contains, and one output mode: list matching sessions, or measure a metric (count, avg_pageviews, conversion_rate, avg_dwell_ms, bounce_rate) optionally grouped by page/source/day/signal/cohort. Pass the query JSON URL-encoded in `q`, or POST it as a body (see the /query/explore POST operation).",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "URL-encoded JSON matching the ExploreQuery schema (window_days, filters, output)"
          }
        ],
        "responses": {
          "200": {
            "description": "Explore query result: a session list or a measured metric",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      },
      "post": {
        "tags": [
          "Read"
        ],
        "summary": "Typed exploratory query over behavioral data (JSON body form)",
        "operationId": "runExplorePost",
        "description": "Same engine as GET /query/explore, with the query passed as a JSON body instead of a URL-encoded query string.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExploreQuery"
              },
              "examples": {
                "list": {
                  "summary": "List confused sessions on /checkout in the last 14 days",
                  "value": {
                    "window_days": 14,
                    "filters": [
                      {
                        "field": "page",
                        "op": "is",
                        "value": "/checkout"
                      },
                      {
                        "field": "confused",
                        "op": "is",
                        "value": true
                      }
                    ],
                    "output": {
                      "mode": "list",
                      "limit": 25
                    }
                  }
                },
                "measure": {
                  "summary": "Conversion rate for rage-click sessions, grouped by source",
                  "value": {
                    "window_days": 30,
                    "filters": [
                      {
                        "field": "signal",
                        "op": "is",
                        "value": "rage_click"
                      }
                    ],
                    "output": {
                      "mode": "measure",
                      "metric": "conversion_rate",
                      "group_by": "source"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Explore query result: a session list or a measured metric",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/query/heuristics": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "The complete signal catalog",
        "operationId": "getHeuristics",
        "description": "Every implemented friction signal type with its scoring weight, whether it decorates journey-friction edges, and its recommendation text.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Signal catalog",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                },
                "example": {
                  "data": {
                    "heuristics": [
                      {
                        "type": "rage_click",
                        "weight": 25,
                        "journey_edge_decorator": true,
                        "recommendation": "Check for a broken or slow-responding action."
                      }
                    ],
                    "total_heuristics": 132
                  },
                  "error": null
                }
              }
            }
          }
        }
      }
    },
    "/query/journeys/friction": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Page-to-page navigation edges weighted by friction",
        "operationId": "getJourneyFriction",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 250
            }
          },
          {
            "name": "min_friction_weight",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000,
              "default": 1
            }
          },
          {
            "name": "signal_type",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Friction-weighted navigation edges",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/raw": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Direct table access with sorting and pagination",
        "operationId": "getRaw",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "table",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "events",
                "signals",
                "sessions",
                "page_scores",
                "score_history",
                "ux_issues",
                "alerts",
                "deploys"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Raw table rows",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/session": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Full event timeline for one session",
        "operationId": "getSession",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "session_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Chronological event timeline for the session",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                },
                "example": {
                  "data": {
                    "session_id": "ses_9b2f1a4c7d3e",
                    "page_count": 4,
                    "signal_count": 11,
                    "events": [
                      {
                        "type": "signal",
                        "signal": "dead_click",
                        "page": "/pricing",
                        "occurred_at": "2026-06-10T13:38:44Z"
                      }
                    ]
                  },
                  "error": null
                }
              }
            }
          }
        }
      }
    },
    "/query/export/events.csv": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "CSV export of raw event rows",
        "operationId": "exportEventsCsv",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "CSV file of events",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/query/mcp/context": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Single-call site context summary for AI assistants",
        "operationId": "getMcpContext",
        "description": "Top scores, open issues, recent deploys, active alerts, and recommendations in one response. Same data the MCP server's get_site_context tool returns.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Site context snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/query/audit": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Organization audit log",
        "operationId": "getAuditLog",
        "description": "Most recent audit log rows for the organization. Org-scoped: takes org_id instead of site_id.",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgId"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Audit log rows, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/query/degradation": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Backend degradation events",
        "operationId": "getDegradation",
        "description": "Active and recent pipeline degradation events affecting this organization (plus any company-global events). Org-scoped: takes org_id instead of site_id.",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgId"
          }
        ],
        "responses": {
          "200": {
            "description": "Active (unresolved) and recent degradation events",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/query/webhook-deliveries": {
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "Outbound webhook delivery attempts",
        "operationId": "getWebhookDeliveries",
        "description": "Recent outbound webhook delivery attempts with status and failure details. Org-scoped: takes org_id instead of site_id.",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgId"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delivery attempts, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/manage/site": {
      "get": {
        "tags": [
          "Write"
        ],
        "summary": "List sites in an organization",
        "operationId": "listSites",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgId"
          }
        ],
        "responses": {
          "200": {
            "description": "Sites in the organization",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Write"
        ],
        "summary": "Create a site",
        "operationId": "createSite",
        "description": "Creates the site plus its production environment, SDK config, and a scoped secret key. Requires a user JWT (owner or admin role).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "org_id",
                  "name",
                  "url"
                ],
                "properties": {
                  "org_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Site created, with fresh publishable and secret keys",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                },
                "example": {
                  "data": {
                    "site": {
                      "id": "7f2c9d4a-4b5e-4c3a-9b1d-2e6a4f8c7d5b",
                      "name": "Marketing site",
                      "url": "https://example.com"
                    },
                    "publishable_key": "fd_pub_xxxxxxxxxxxx",
                    "secret_key": "fd_sec_xxxxxxxxxxxx"
                  },
                  "error": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/manage/site/{site_id}": {
      "patch": {
        "tags": [
          "Write"
        ],
        "summary": "Update a site",
        "operationId": "updateSite",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteIdPath"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "paused",
                      "degraded"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated site",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Write"
        ],
        "summary": "Delete a site",
        "operationId": "deleteSite",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteIdPath"
          }
        ],
        "responses": {
          "200": {
            "description": "Site deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/manage/alert-rules": {
      "get": {
        "tags": [
          "Write"
        ],
        "summary": "List alert rules for a site",
        "operationId": "listAlertRules",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Alert rules",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Write"
        ],
        "summary": "Create an alert rule",
        "operationId": "createAlertRule",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AlertRuleInput"
              },
              "example": {
                "site_id": "7f2c9d4a-4b5e-4c3a-9b1d-2e6a4f8c7d5b",
                "trigger_type": "spike",
                "threshold": 25,
                "cooldown_minutes": 60,
                "channels": [
                  "email",
                  "slack"
                ],
                "config": {
                  "page_pattern": "/checkout*"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Alert rule created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/manage/alert-rules/{rule_id}": {
      "patch": {
        "tags": [
          "Write"
        ],
        "summary": "Update an alert rule",
        "operationId": "updateAlertRule",
        "parameters": [
          {
            "name": "rule_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "threshold": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 1000
                  },
                  "channels": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "email",
                        "slack",
                        "webhook",
                        "mcp",
                        "pagerduty"
                      ]
                    }
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated alert rule",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Write"
        ],
        "summary": "Delete an alert rule",
        "operationId": "deleteAlertRule",
        "description": "Permanent. Use PATCH with enabled:false to silence a rule instead.",
        "parameters": [
          {
            "name": "rule_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Alert rule deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/manage/alerts/{alert_id}": {
      "patch": {
        "tags": [
          "Write"
        ],
        "summary": "Acknowledge, investigate, or resolve an alert",
        "operationId": "updateAlertStatus",
        "parameters": [
          {
            "name": "alert_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "acknowledged",
                      "investigating",
                      "resolved"
                    ]
                  },
                  "resolved_reason": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated alert",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/manage/issues/{issue_id}": {
      "patch": {
        "tags": [
          "Write"
        ],
        "summary": "Triage, assign, or resolve a UX issue",
        "operationId": "updateIssue",
        "parameters": [
          {
            "$ref": "#/components/parameters/IssueIdPath"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "open",
                      "triaged",
                      "in_progress",
                      "verified",
                      "resolved",
                      "ignored"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 1000
                  },
                  "assigned_to": {
                    "type": "string",
                    "maxLength": 256
                  },
                  "frozen": {
                    "type": "boolean",
                    "description": "Pins the issue so nightly AI triage can't merge, dismiss, or rewrite it."
                  }
                }
              },
              "example": {
                "status": "triaged",
                "note": "Confirmed on Safari iOS 17. Related to the disabled-state border color.",
                "assigned_to": "alex"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated issue",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/manage/members": {
      "get": {
        "tags": [
          "Write"
        ],
        "summary": "List organization members",
        "operationId": "listMembers",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgId"
          }
        ],
        "responses": {
          "200": {
            "description": "Members",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Write"
        ],
        "summary": "Invite a member",
        "operationId": "inviteMember",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "org_id",
                  "email",
                  "role"
                ],
                "properties": {
                  "org_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "member",
                      "viewer"
                    ],
                    "description": "owner cannot be granted through invite"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Invite sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/manage/members/{member_id}": {
      "patch": {
        "tags": [
          "Write"
        ],
        "summary": "Change a member's role",
        "operationId": "updateMember",
        "description": "Owner-only. A user cannot change their own role.",
        "parameters": [
          {
            "name": "member_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "admin",
                      "member",
                      "viewer"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated member",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Write"
        ],
        "summary": "Remove a member",
        "operationId": "removeMember",
        "description": "Owner-only. A user cannot remove themselves.",
        "parameters": [
          {
            "name": "member_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Member removed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/manage/webhooks": {
      "get": {
        "tags": [
          "Write"
        ],
        "summary": "List outbound webhook endpoints",
        "operationId": "listWebhooks",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgId"
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Write"
        ],
        "summary": "Create an outbound webhook endpoint",
        "operationId": "createWebhook",
        "description": "Deliveries are signed HMAC-SHA256 (x-flusterduck-signature) with retry and dedup. The signing secret is returned once, at creation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "org_id",
                  "url"
                ],
                "properties": {
                  "org_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "site_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "issue.created",
                        "issue.updated",
                        "alert.triggered",
                        "alert.acknowledged",
                        "alert.resolved",
                        "score.spike",
                        "deploy.recorded"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                },
                "example": {
                  "data": {
                    "endpoint": {
                      "id": "wh_123",
                      "url": "https://example.com/hooks/flusterduck"
                    },
                    "signing_secret": "whsec_fd_xxxxxxxxxxxx"
                  },
                  "error": null
                }
              }
            }
          }
        }
      }
    },
    "/manage/webhooks/{endpoint_id}": {
      "patch": {
        "tags": [
          "Write"
        ],
        "summary": "Update a webhook endpoint",
        "operationId": "updateWebhook",
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "active": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated webhook",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Write"
        ],
        "summary": "Disable a webhook endpoint",
        "operationId": "deleteWebhook",
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook disabled",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/manage/integrations": {
      "get": {
        "tags": [
          "Write"
        ],
        "summary": "List connected integrations",
        "operationId": "listIntegrations",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgId"
          }
        ],
        "responses": {
          "200": {
            "description": "Connected integrations plus OAuth availability",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Write"
        ],
        "summary": "Connect an integration",
        "operationId": "createIntegration",
        "description": "Slack and PagerDuty connect with a pasted webhook URL / routing key. Slack, Linear, and GitHub can also connect via OAuth (see /manage/integrations/oauth-start).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "org_id",
                  "provider"
                ],
                "properties": {
                  "org_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "provider": {
                    "type": "string",
                    "enum": [
                      "slack",
                      "pagerduty",
                      "linear",
                      "github"
                    ]
                  },
                  "incoming_webhook_url": {
                    "type": "string",
                    "description": "Slack only: must start with https://hooks.slack.com/"
                  },
                  "routing_key": {
                    "type": "string",
                    "description": "PagerDuty only: 32-character alphanumeric routing key"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Integration connected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/manage/integrations/{connection_id}": {
      "delete": {
        "tags": [
          "Write"
        ],
        "summary": "Disconnect an integration",
        "operationId": "deleteIntegration",
        "parameters": [
          {
            "name": "connection_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Integration disconnected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/manage/annotations": {
      "post": {
        "tags": [
          "Write"
        ],
        "summary": "Write a timeline annotation",
        "operationId": "createAnnotation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "site_id",
                  "message"
                ],
                "properties": {
                  "site_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "message": {
                    "type": "string",
                    "maxLength": 1000
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "manual",
                      "auto",
                      "deploy",
                      "billing",
                      "mcp"
                    ],
                    "default": "manual"
                  }
                }
              },
              "example": {
                "site_id": "7f2c9d4a-4b5e-4c3a-9b1d-2e6a4f8c7d5b",
                "message": "Redesigned checkout flow, monitoring score"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Annotation created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/manage/keys": {
      "get": {
        "tags": [
          "Write"
        ],
        "summary": "List API keys for a site",
        "operationId": "listKeys",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Keys (hash never returned)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Write"
        ],
        "summary": "Create a secret or MCP key",
        "operationId": "createKey",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "site_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "key_type": {
                    "type": "string",
                    "enum": [
                      "secret",
                      "mcp"
                    ]
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "query:read",
                        "manage:write",
                        "mcp:read",
                        "webhook:write"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created. The raw key is returned once and never again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                },
                "example": {
                  "data": {
                    "key": {
                      "id": "key_123",
                      "key_type": "secret",
                      "key_prefix": "fd_sec_ab12"
                    },
                    "raw_key": "fd_sec_xxxxxxxxxxxx"
                  },
                  "error": null
                }
              }
            }
          }
        }
      }
    },
    "/manage/keys/{key_id}": {
      "delete": {
        "tags": [
          "Write"
        ],
        "summary": "Revoke an API key",
        "operationId": "revokeKey",
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Key revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    },
    "/deploys": {
      "post": {
        "tags": [
          "Write"
        ],
        "summary": "Record a deploy for before/after confusion verification",
        "description": "Records a deploy so Flusterduck captures a confusion-before snapshot and, after ~5 minutes of post-deploy traffic, a confusion-after snapshot. Open issues on affected pages get verification records. Requires an fd_sec_ secret key. The CLI equivalent is `flusterduck deploy notify --site <site_id>`.",
        "operationId": "recordDeploy",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "site_id"
                ],
                "properties": {
                  "site_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "provider": {
                    "type": "string",
                    "enum": [
                      "github",
                      "vercel",
                      "gitlab",
                      "bitbucket",
                      "custom"
                    ],
                    "default": "custom"
                  },
                  "environment": {
                    "type": "string",
                    "maxLength": 64,
                    "default": "production"
                  },
                  "commit_hash": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "commit_message": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "author": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "external_deploy_id": {
                    "type": "string",
                    "maxLength": 160
                  },
                  "pr_number": {
                    "type": "string",
                    "maxLength": 32
                  },
                  "metadata": {
                    "type": "object"
                  }
                }
              },
              "example": {
                "site_id": "7f2c9d4a-4b5e-4c3a-9b1d-2e6a4f8c7d5b",
                "provider": "github",
                "commit_hash": "abc1234",
                "environment": "production"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recorded deploy with its confusion_before snapshot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Read"
        ],
        "summary": "List deploys (alias of /query/deploys)",
        "operationId": "listDeploysPublic",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deploys with confusion_before, confusion_after, and related issues",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataEnvelope"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "fd_sec_... secret key, fd_mcp_... MCP key, or a Supabase user JWT",
        "description": "Pass `Authorization: Bearer fd_sec_...` (a secret key created in Settings \u2192 API Keys, scoped to query:read and/or manage:write), `Authorization: Bearer fd_mcp_...` (an MCP key, read-only unless it also carries manage:write), or `Authorization: Bearer <user JWT>` from a signed-in session. Site-creation and member-management routes require a user JWT."
      }
    },
    "parameters": {
      "SiteId": {
        "name": "site_id",
        "in": "query",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "The site to query. Required on every /query route except org-scoped routes (audit, degradation, webhook-deliveries)."
      },
      "SiteIdPath": {
        "name": "site_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "OrgId": {
        "name": "org_id",
        "in": "query",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "Page": {
        "name": "page",
        "in": "query",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Page path, e.g. /checkout"
      },
      "PageOptional": {
        "name": "page",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Page path, e.g. /checkout. Omit for a site-wide view."
      },
      "IssueId": {
        "name": "issue_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "IssueIdPath": {
        "name": "issue_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, expired, or malformed key/JWT",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Missing required field or invalid value",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource doesn't exist or belongs to a different site/org",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. See Retry-After header.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      }
    },
    "schemas": {
      "DataEnvelope": {
        "type": "object",
        "properties": {
          "data": {
            "description": "Endpoint-specific payload",
            "type": [
              "object",
              "array",
              "null"
            ]
          },
          "error": {
            "type": "null"
          }
        },
        "required": [
          "data",
          "error"
        ]
      },
      "ErrorEnvelope": {
        "type": "object",
        "properties": {
          "data": {
            "type": "null"
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "status": {
                "type": "integer",
                "description": "HTTP status code, repeated in the body"
              },
              "details": {
                "description": "Extra context for the error. Omitted when there is none."
              }
            },
            "required": [
              "code",
              "message",
              "status"
            ]
          }
        },
        "required": [
          "data",
          "error"
        ]
      },
      "AlertRuleInput": {
        "type": "object",
        "required": [
          "site_id",
          "trigger_type",
          "threshold"
        ],
        "properties": {
          "site_id": {
            "type": "string",
            "format": "uuid"
          },
          "trigger_type": {
            "type": "string",
            "enum": [
              "spike",
              "anomaly",
              "new_page",
              "trend",
              "co_occurrence",
              "positive",
              "budget",
              "revenue_threshold"
            ]
          },
          "threshold": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000
          },
          "cooldown_minutes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1440,
            "default": 60
          },
          "channels": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "email",
                "slack",
                "webhook",
                "mcp",
                "pagerduty"
              ]
            }
          },
          "config": {
            "type": "object",
            "description": "Trigger-specific config, e.g. page_pattern, slack_channel"
          }
        }
      },
      "ExploreQuery": {
        "type": "object",
        "required": [
          "window_days",
          "filters",
          "output"
        ],
        "properties": {
          "window_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 90
          },
          "filters": {
            "type": "array",
            "maxItems": 12,
            "items": {
              "type": "object",
              "required": [
                "field",
                "op",
                "value"
              ],
              "properties": {
                "field": {
                  "type": "string",
                  "enum": [
                    "signal",
                    "page",
                    "source",
                    "confused",
                    "converted",
                    "event_type"
                  ]
                },
                "op": {
                  "type": "string",
                  "enum": [
                    "is",
                    "is_not",
                    "contains"
                  ]
                },
                "value": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "boolean"
                    }
                  ]
                }
              }
            }
          },
          "output": {
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "mode"
                ],
                "properties": {
                  "mode": {
                    "const": "list"
                  },
                  "limit": {
                    "type": "integer"
                  },
                  "offset": {
                    "type": "integer"
                  }
                }
              },
              {
                "type": "object",
                "required": [
                  "mode",
                  "metric"
                ],
                "properties": {
                  "mode": {
                    "const": "measure"
                  },
                  "metric": {
                    "type": "string",
                    "enum": [
                      "count",
                      "avg_pageviews",
                      "conversion_rate",
                      "avg_dwell_ms",
                      "bounce_rate"
                    ]
                  },
                  "group_by": {
                    "type": "string",
                    "enum": [
                      "page",
                      "source",
                      "day",
                      "signal",
                      "cohort"
                    ]
                  }
                }
              }
            ]
          }
        }
      }
    }
  }
}
