{
  "openapi": "3.1.0",
  "info": {
    "title": "TagSentry API",
    "version": "2026-08-26",
    "summary": "Set up consent and event monitoring without opening the dashboard.",
    "description": "Everything after sign-up is meant to be reachable by a script or an agent.\n\n**Authentication.** A bearer API key: `Authorization: Bearer tsk_live_...` (or `tsk_test_...`). Keys are minted from the dashboard by a member of the account, or issued to a terminal through the device authorization grant below, and carry an explicit, non-implied list of scopes. A key can never mint another key.\n\n**Test keys.** The prefix names the deployment that minted the key, and nothing reads it after that. `tsk_live_`: Minted by a production or staging deployment, app.tagsentry.ai included. The prefix a secret scanner should match. `tsk_test_`: Minted by a development or self-hosted deployment without a production APP_ENV. Not a sandbox: it has the same scopes and reaches the same real data as a live key, on the deployment that minted it. No other deployment knows it. There is no sandbox mode: to test without touching a real site, create a throwaway site and archive it afterwards.\n\n**The one exception.** `POST /device-authorizations` and `POST /device-authorizations/token` take no credential -- they are how something with no key gets one, with a human approving in a browser. Every other operation on this surface requires a key.\n\n**Tenancy.** The account a request acts for comes from the key and from nowhere else -- never from a path, query or body. A site id belonging to another account resolves to 404.\n\n**Regions.** A site is created in EU or US and its consent records and tag events live there. The region is a property of the site, read from our index; it is never accepted from the request. Responses that read regional data carry `X-TagSentry-Region`.\n\n**Truncation.** Every list response carries `truncated`. When it is true you are not looking at the whole answer. Treat it as an error condition for anything that has to be complete.\n\n**Rate limits.** Per key and per account, per minute; banner writes also per site. A 429 names which budget and carries `Retry-After`. If we cannot check a budget we refuse rather than guess.\n\n**Strict Content-Security-Policy.** `GET /sites/{siteId}/install` returns `csp`: the origins, the hash of the one-line tag's onerror handler (it needs `'unsafe-hashes'`), and this site's inline-snippet hashes.\n\n**Tag Manager is read only here.** No operation in this version writes to a customer's container; changes are approved by a person in the dashboard.\n\n**Webhooks.** Added per business in the dashboard (API keys page) for `consent.recorded` (batched, at most one POST a minute per site, counts and record ids, never visitor data), `ruleset.published` and `scan.completed`, plus `ping` from Send test. Each POST is signed: `X-TagSentry-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of \"t.body\">` with the endpoint's secret. Refuse a `t` more than 5 minutes old. `X-TagSentry-Delivery` is stable across retries; dedupe on it. The `webhooks` section of this document has each body and a verification snippet.",
    "contact": {
      "name": "TagSentry",
      "url": "https://tagsentry.ai"
    }
  },
  "servers": [
    {
      "url": "/api/v1",
      "description": "This deployment."
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "tsk_live_… / tsk_test_…",
        "description": "An API key. Scopes are additive and never implied: a key that needs to both read and create sites carries `sites:read` AND `sites:write`.\n\n- `account:read` — Read which account this key belongs to: its id, its name and whether a person has confirmed that name. Carries no region (each site names its own when it is created), no billing and no member list.\n- `sites:read` — List sites and read their setup state.\n- `sites:write` — Create sites and archive them.\n- `scans:write` — Start a scan of a site.\n- `trackers:read` — Read the detected tracker inventory and its classification.\n- `monitoring:read` — Read tag events, event-name summaries and the tag flow.\n- `consent:read` — Read consent decisions and their rates.\n- `consent:export` — Download the proof-of-consent export.\n- `install:read` — Read the install snippet, the site key and the delivery origin.\n- `banner:write` — Change and publish the consent banner and its ruleset.\n- `gtm:read` — Read the connected Tag Manager container and its tag inventory.\n- `gtm:write` — Stage and publish a consent version into the customer's Tag Manager container. Additionally requires a verified domain and a live step-up OAuth grant that only a human can create. No API operation uses it yet.\n- `billing:read` — Read whether each site is Free or Pro in each product, its usage and payment state. Owner-only."
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "unauthorized",
                  "forbidden",
                  "domain_not_verified",
                  "not_found",
                  "invalid_request",
                  "conflict",
                  "rate_limited",
                  "not_implemented",
                  "internal"
                ]
              },
              "message": {
                "type": "string"
              },
              "detail": {},
              "requestId": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "message",
              "requestId"
            ],
            "additionalProperties": false
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "description": "Every non-2xx response from this API has this shape."
      }
    }
  },
  "tags": [
    {
      "name": "Getting a key",
      "description": "How a process with no credential gets one: it shows a human a code, the human approves it in a browser, and the process polls. Start here if you are a CLI, an agent, or anything else running where nobody can paste a secret in."
    },
    {
      "name": "Sites",
      "description": "Sites, and everything measured on one."
    },
    {
      "name": "Account",
      "description": "The account this key belongs to."
    },
    {
      "name": "Webhooks",
      "description": "What we POST to your endpoints, and how to verify it came from us."
    }
  ],
  "paths": {
    "/sites": {
      "get": {
        "operationId": "listSites",
        "summary": "List sites",
        "description": "Every site on the account this key belongs to. Archived sites are excluded unless you ask for them.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "sites:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "includeArchived",
            "in": "query",
            "required": false,
            "description": "Include archived sites. Defaults to false.",
            "schema": {
              "description": "Include archived sites. Defaults to false.",
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List sites",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "The site's id. This is what every /v1/sites/{siteId} path takes."
                          },
                          "accountId": {
                            "type": "string"
                          },
                          "domain": {
                            "type": "string"
                          },
                          "displayName": {
                            "type": "string"
                          },
                          "region": {
                            "type": "string",
                            "enum": [
                              "EU",
                              "US"
                            ],
                            "description": "Where this site's consent records and tag events are stored. Fixed at creation and never editable -- data residency is not a setting you can flip."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "active",
                              "archived"
                            ]
                          },
                          "archivedAt": {
                            "anyOf": [
                              {
                                "type": "string",
                                "description": "ISO-8601 timestamp, UTC."
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "createdAt": {
                            "type": "string",
                            "description": "ISO-8601 timestamp, UTC."
                          },
                          "updatedAt": {
                            "type": "string",
                            "description": "ISO-8601 timestamp, UTC."
                          }
                        },
                        "required": [
                          "id",
                          "accountId",
                          "domain",
                          "displayName",
                          "region",
                          "status",
                          "archivedAt",
                          "createdAt",
                          "updatedAt"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "limit": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "The row cap applied to the underlying read."
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when the read came back at its cap. When true, this page is NOT the whole answer -- narrow the window. An account's site list has no read cap -- `truncated` is always false here, and is present so the envelope's shape never varies by endpoint."
                    },
                    "truncationHint": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "data",
                    "limit",
                    "truncated"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createSite",
        "summary": "Create a site",
        "description": "Adds a site to this account. The domain is not checked for ownership here and the site is usable immediately -- verification is a separate state that gates acting outward on the domain's behalf, never the site's existence. `region` is fixed at creation and can never be changed: it decides where this site's consent records and tag events are stored.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "sites:write"
            ]
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string"
                  },
                  "displayName": {
                    "type": "string",
                    "minLength": 1
                  },
                  "region": {
                    "type": "string",
                    "enum": [
                      "EU",
                      "US"
                    ]
                  }
                },
                "required": [
                  "domain",
                  "displayName",
                  "region"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Create a site",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "The site's id. This is what every /v1/sites/{siteId} path takes."
                    },
                    "accountId": {
                      "type": "string"
                    },
                    "domain": {
                      "type": "string"
                    },
                    "displayName": {
                      "type": "string"
                    },
                    "region": {
                      "type": "string",
                      "enum": [
                        "EU",
                        "US"
                      ],
                      "description": "Where this site's consent records and tag events are stored. Fixed at creation and never editable -- data residency is not a setting you can flip."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "active",
                        "archived"
                      ]
                    },
                    "archivedAt": {
                      "anyOf": [
                        {
                          "type": "string",
                          "description": "ISO-8601 timestamp, UTC."
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "createdAt": {
                      "type": "string",
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "updatedAt": {
                      "type": "string",
                      "description": "ISO-8601 timestamp, UTC."
                    }
                  },
                  "required": [
                    "id",
                    "accountId",
                    "domain",
                    "displayName",
                    "region",
                    "status",
                    "archivedAt",
                    "createdAt",
                    "updatedAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The request conflicts with existing state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/archive": {
      "post": {
        "operationId": "archiveSite",
        "summary": "Archive a site",
        "description": "Takes a site out of the account: it leaves the site list, its banner stops being served, and its per-site billing line is released. The same archive the dashboard's Remove this site makes. To finish undoing an install, take the code off your pages too: until you do, it keeps setting denied Consent Mode defaults and shows no banner.\n\nNOTHING IS DELETED. The site's consent records, tag events and configuration stay in its region for their retention period, because a consent record is the legal proof of what a visitor chose. `GET /sites?includeArchived=true` still lists it.\n\nIDEMPOTENT. Archiving an archived site answers 200 with the same body and changes nothing.\n\nA site with a paid product can be archived only by a key an account OWNER minted, because it changes the bill: the dashboard's rule, read from the minter's current role. A member's key gets 403 for it and may archive a site that pays for nothing.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "sites:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Archive a site",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "The site's id. This is what every /v1/sites/{siteId} path takes."
                    },
                    "accountId": {
                      "type": "string"
                    },
                    "domain": {
                      "type": "string"
                    },
                    "displayName": {
                      "type": "string"
                    },
                    "region": {
                      "type": "string",
                      "enum": [
                        "EU",
                        "US"
                      ],
                      "description": "Where this site's consent records and tag events are stored. Fixed at creation and never editable -- data residency is not a setting you can flip."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "active",
                        "archived"
                      ]
                    },
                    "archivedAt": {
                      "anyOf": [
                        {
                          "type": "string",
                          "description": "ISO-8601 timestamp, UTC."
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "createdAt": {
                      "type": "string",
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "updatedAt": {
                      "type": "string",
                      "description": "ISO-8601 timestamp, UTC."
                    }
                  },
                  "required": [
                    "id",
                    "accountId",
                    "domain",
                    "displayName",
                    "region",
                    "status",
                    "archivedAt",
                    "createdAt",
                    "updatedAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/tag-events": {
      "get": {
        "operationId": "listTagEvents",
        "summary": "Read tag events",
        "description": "What actually fired on this site, newest first: page views, the events that occurred on them, the tags that ran and the requests those tags sent. Served from the site's own region -- see the X-TagSentry-Region response header.\n\nThis is a time WINDOW, not a paged list. If `truncated` is true the window was too wide and you are NOT looking at the whole answer; narrow `from`/`to`.\n\nZero events does not mean the snippet is not installed. Monitoring runs only after a consent decision that grants the category it rides on, so a correct install on a site whose visitors reject analytics reports zero forever, correctly. The only honest reading of zero is \"we have not heard from it yet\".",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "monitoring:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "ISO-8601. Inclusive lower bound on receivedAt.",
            "schema": {
              "description": "ISO-8601. Inclusive lower bound on receivedAt.",
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "ISO-8601. Inclusive upper bound on receivedAt.",
            "schema": {
              "description": "ISO-8601. Inclusive upper bound on receivedAt.",
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Row cap. Capped by the service's own ceiling.",
            "schema": {
              "description": "Row cap. Capped by the service's own ceiling.",
              "type": "string"
            }
          },
          {
            "name": "batchId",
            "in": "query",
            "required": false,
            "description": "One page view's rows.",
            "schema": {
              "description": "One page view's rows.",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read tag events",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "siteId": {
                            "type": "string"
                          },
                          "batchId": {
                            "type": "string",
                            "description": "One page view. Rows sharing a batchId happened on the same page."
                          },
                          "seq": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "Order within the batch. Seq 0 is always the batch row."
                          },
                          "kind": {
                            "type": "string",
                            "description": "batch | tag | event | request"
                          },
                          "receivedAt": {
                            "type": "string",
                            "description": "Our clock. The only orderable time here -- a client's is never trusted."
                          },
                          "pageMs": {
                            "anyOf": [
                              {
                                "type": "integer",
                                "minimum": -9007199254740991,
                                "maximum": 9007199254740991
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "subjectId": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "Present on the batch row only. Links the page view to the consent decision that permitted it."
                          },
                          "containerId": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "tagId": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "status": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "executionMs": {
                            "anyOf": [
                              {
                                "type": "integer",
                                "minimum": -9007199254740991,
                                "maximum": 9007199254740991
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "eventName": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "ordinal": {
                            "anyOf": [
                              {
                                "type": "integer",
                                "minimum": -9007199254740991,
                                "maximum": 9007199254740991
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "host": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "origin": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "path": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "initiatorType": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "paramNames": {
                            "anyOf": [
                              {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "occurrences": {
                            "anyOf": [
                              {
                                "type": "integer",
                                "minimum": -9007199254740991,
                                "maximum": 9007199254740991
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "transferBytes": {
                            "anyOf": [
                              {
                                "type": "integer",
                                "minimum": -9007199254740991,
                                "maximum": 9007199254740991
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "durationMs": {
                            "anyOf": [
                              {
                                "type": "integer",
                                "minimum": -9007199254740991,
                                "maximum": 9007199254740991
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "buffered": {
                            "anyOf": [
                              {
                                "type": "boolean"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "truncated": {
                            "anyOf": [
                              {
                                "type": "boolean"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          }
                        },
                        "required": [
                          "id",
                          "siteId",
                          "batchId",
                          "seq",
                          "kind",
                          "receivedAt",
                          "pageMs",
                          "subjectId",
                          "containerId",
                          "tagId",
                          "status",
                          "executionMs",
                          "eventName",
                          "ordinal",
                          "host",
                          "origin",
                          "path",
                          "initiatorType",
                          "paramNames",
                          "occurrences",
                          "transferBytes",
                          "durationMs",
                          "buffered",
                          "truncated"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "limit": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "The row cap applied to the underlying read."
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when the read came back at its cap. When true, this page is NOT the whole answer -- narrow the window. Tag events are a time window, not a paged list."
                    },
                    "truncationHint": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "data",
                    "limit",
                    "truncated"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/consent-records": {
      "get": {
        "operationId": "listConsentRecords",
        "summary": "Read consent decisions",
        "description": "This site's consent records, oldest first. Served from the site's own region.\n\nThese are the legal record of what each visitor was shown and what they chose. They are never edited and never deleted inside their retention period.\n\nIf `truncated` is true this is a PARTIAL answer -- narrow `from`/`to` and page through by time. Do not treat a truncated response as a complete history; for a regulator-facing artifact use the export endpoint, not this one.\n\nOnly for a site whose domain is verified: otherwise the answer is 403 `domain_not_verified`, and verifying the domain is the fix.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "consent:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "ISO-8601. Inclusive lower bound on receivedAt.",
            "schema": {
              "description": "ISO-8601. Inclusive lower bound on receivedAt.",
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "ISO-8601. Inclusive upper bound on receivedAt.",
            "schema": {
              "description": "ISO-8601. Inclusive upper bound on receivedAt.",
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Row cap. Capped by the service's own ceiling.",
            "schema": {
              "description": "Row cap. Capped by the service's own ceiling.",
              "type": "string"
            }
          },
          {
            "name": "subjectId",
            "in": "query",
            "required": false,
            "description": "One visitor's decision history.",
            "schema": {
              "description": "One visitor's decision history.",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read consent decisions",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "recordVersion": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991
                          },
                          "id": {
                            "type": "string",
                            "description": "Deterministic and derived, not random -- the beacon may retry, and the legal record must not double-count."
                          },
                          "subjectId": {
                            "type": "string"
                          },
                          "siteId": {
                            "type": "string"
                          },
                          "rulesetIntegrity": {
                            "type": "string",
                            "description": "Ties the decision to the exact banner configuration that was shown."
                          },
                          "jurisdiction": {
                            "type": "string"
                          },
                          "jurisdictionReason": {
                            "type": "string",
                            "description": "How we placed the visitor. A no_signal fallback is a weaker claim than a determination, and this says which it was."
                          },
                          "method": {
                            "type": "string"
                          },
                          "decidedAt": {
                            "type": "string",
                            "description": "From the visitor's device. A CLAIM."
                          },
                          "receivedAt": {
                            "type": "string",
                            "description": "Stamped by us. Authoritative for ordering and retention."
                          },
                          "granted": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "denied": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "signals": {
                            "type": "object",
                            "propertyNames": {
                              "type": "string"
                            },
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "The Consent Mode v2 signals actually applied."
                          },
                          "shown": {
                            "description": "What banner the visitor was shown."
                          }
                        },
                        "required": [
                          "recordVersion",
                          "id",
                          "subjectId",
                          "siteId",
                          "rulesetIntegrity",
                          "jurisdiction",
                          "jurisdictionReason",
                          "method",
                          "decidedAt",
                          "receivedAt",
                          "granted",
                          "denied",
                          "signals",
                          "shown"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "limit": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "The row cap applied to the underlying read."
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when the read came back at its cap. When true, this page is NOT the whole answer -- narrow the window. Consent records are returned oldest-first. Narrow the window with `from`/`to`."
                    },
                    "truncationHint": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "data",
                    "limit",
                    "truncated"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/account": {
      "get": {
        "operationId": "getAccount",
        "summary": "Read this account",
        "description": "Which account this key acts for. The first call a wizard or an agent should make: it turns an opaque credential into a name a human can confirm before anything is created under it.\n\nCarries no billing state and no member list -- those are a different scope and, in the case of the member list, not something a machine credential should enumerate.",
        "tags": [
          "Account"
        ],
        "security": [
          {
            "apiKey": [
              "account:read"
            ]
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Read this account",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accountId": {
                      "type": "string",
                      "description": "The account this key acts for. It comes from the key and from nowhere else -- no path, query or body can name a different one."
                    },
                    "name": {
                      "type": "string"
                    },
                    "onboarded": {
                      "type": "boolean",
                      "description": "False while nobody has confirmed this account's name. A wizard should say the name is a guess rather than print it as a choice somebody made."
                    },
                    "createdAt": {
                      "type": "string",
                      "description": "ISO-8601 timestamp, UTC."
                    }
                  },
                  "required": [
                    "accountId",
                    "name",
                    "onboarded",
                    "createdAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/install": {
      "get": {
        "operationId": "getSiteInstall",
        "summary": "Read the install snippet",
        "description": "Everything needed to install this site: the exact snippet bytes, the public site key, the delivery origin for a CSP entry, and the URLs the client will talk to.\n\nORDER IS THE CONTRACT. The snippet goes in <head>, ABOVE the tag manager snippet, and below nothing except whatever sets the visitor's jurisdiction. It never loads, defers or gates gtm.js -- a total delivery failure of ours leaves the customer with a loaded container, denied Consent Mode defaults and no banner, which is fail-closed and visible.\n\nThis call MINTS what it has to: a site that has never had a public key gets one here, and a site with no ruleset configuration gets the default one published. That is a write on a GET, deliberately -- the customer's whole obligation is to paste one snippet, and an endpoint that asked them to press Generate first would have added an obligation to get the thing that was supposed to be their only one.\n\nSTRICT CONTENT-SECURITY-POLICY. `csp` lists what to allow, read off these exact bytes. The one-line `siteTag` has an inline `onerror` that sets denied Consent Mode defaults if our script cannot load; a policy admits it only with `'unsafe-hashes'` and its hash (`csp.siteTagScriptSrc`, the same on every site). Or use the inline `snippet`, which makes no request before your tags and needs one hash per inline block (`csp.snippetScriptSrc`, this site's own, changed by any banner or blocking change). The tag also works with its onerror removed, and then it FAILS OPEN: if our script cannot load (an outage, a blocked host), nothing sets the denied Consent Mode defaults, Google tags read the unset signals as granted, and they fire with full storage on every page view until the script loads again. Only the Tag Manager consent template, which sets its own denied defaults inside the container, still holds the line. Prefer the hash, or the inline snippet. Either way the banner needs `style-src 'unsafe-inline'` and the origins in script-src, connect-src and img-src.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "install:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read the install snippet",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "siteId": {
                      "type": "string"
                    },
                    "siteKey": {
                      "type": "string",
                      "description": "The site's public key. It appears in every visitor's HTML, so it is public by construction -- put it in a client-side environment variable, not a secret store."
                    },
                    "region": {
                      "type": "string",
                      "enum": [
                        "EU",
                        "US"
                      ]
                    },
                    "snippet": {
                      "type": "string",
                      "description": "The exact bytes to place in <head>. ORDER IS THE WHOLE CONTRACT: this goes ABOVE the tag manager snippet and below nothing except whatever sets the visitor's jurisdiction. It never loads, defers or gates gtm.js."
                    },
                    "siteTag": {
                      "description": "The one-line install (the default since 2026-09-29): one synchronous script tag naming this site's script, with a fail-closed onerror. Same place as `snippet`, never async or defer.",
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "deliveryOrigin": {
                      "type": "string",
                      "description": "Where the client artifacts are served from, for a Content-Security-Policy entry."
                    },
                    "rulesetUrl": {
                      "type": "string",
                      "description": "The site's compiled ruleset, which the payload fetches."
                    },
                    "consentIngestUrl": {
                      "type": "string"
                    },
                    "monitoringIngestUrl": {
                      "type": "string"
                    },
                    "parts": {
                      "type": "object",
                      "properties": {
                        "configJs": {
                          "type": "string",
                          "description": "Empty since 2026-09-29: the site's config rides in the bootstrap's call. Emit it first if non-empty."
                        },
                        "bootstrapJs": {
                          "type": "string",
                          "description": "The inline bootstrap, its call carrying the site's config. Inline, first."
                        },
                        "tcfStubJs": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "consentModeJs": {
                          "description": "Present only when the site opts into Consent Mode extras. Inline, after the bootstrap.",
                          "type": "string"
                        },
                        "blocker": {
                          "description": "Present only when the site blocks trackers outside Tag Manager. A SYNCHRONOUS script element with this src, integrity and crossorigin=\"anonymous\", directly after the bootstrap -- never async, deferred or injected -- or hardcoded trackers run before consent. (Until 2026-09-29 this was `blockerJs`, an inline script.)",
                          "type": "object",
                          "properties": {
                            "url": {
                              "type": "string"
                            },
                            "integrity": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "url",
                            "integrity"
                          ],
                          "additionalProperties": false
                        },
                        "payload": {
                          "anyOf": [
                            {
                              "type": "object",
                              "properties": {
                                "kind": {
                                  "type": "string",
                                  "const": "tag"
                                },
                                "url": {
                                  "type": "string"
                                },
                                "integrity": {
                                  "type": "string"
                                }
                              },
                              "required": [
                                "kind",
                                "url",
                                "integrity"
                              ],
                              "additionalProperties": false
                            },
                            {
                              "type": "object",
                              "properties": {
                                "kind": {
                                  "type": "string",
                                  "const": "loader"
                                },
                                "js": {
                                  "type": "string"
                                }
                              },
                              "required": [
                                "kind",
                                "js"
                              ],
                              "additionalProperties": false
                            }
                          ]
                        }
                      },
                      "required": [
                        "configJs",
                        "bootstrapJs",
                        "tcfStubJs",
                        "payload"
                      ],
                      "additionalProperties": false,
                      "description": "THE SAME SNIPPET, IN PIECES, for a caller with no HTML file to paste into -- a React root layout, a template helper, a framework plugin. Emit them in the order the order the snippet uses: config, bootstrap, blocker and Consent Mode line if present, TCF stub if present, payload last.\n\nThe two inline pieces must run before anything else on the page and make no network request; the payload is async and must never block. Getting that wrong does not break the page -- it breaks the Consent Mode race the bootstrap exists to win, silently, on somebody else's visitors."
                    },
                    "ready": {
                      "type": "boolean",
                      "description": "False when something upstream of the paste is missing -- most often that no ruleset has been published yet. `problems` says which, in words meant for a developer."
                    },
                    "problems": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "monitoringReady": {
                      "description": "True when `siteTag` runs EVENT MONITORING as soon as it is on the page, whatever `ready` says about the banner: the same one line serves both products, so a monitoring-only site installs exactly this line too (it shows no banner while the banner is offline or another consent tool runs the page, and monitors only where the visitor's consent allows).",
                      "type": "boolean"
                    },
                    "csp": {
                      "description": "What a strict Content-Security-Policy must allow for this install. Absent when `ready` is false. Merge each list into your own directive; nothing here replaces your policy.",
                      "type": "object",
                      "properties": {
                        "origins": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Every origin our client loads from or sends to."
                        },
                        "siteTagScriptSrc": {
                          "anyOf": [
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            {
                              "type": "null"
                            }
                          ],
                          "description": "script-src for the one-line tag: the origins, `'unsafe-hashes'` and the hash of its onerror handler. The hash is the same on every site. A nonce cannot cover the onerror attribute."
                        },
                        "snippetScriptSrc": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "script-src for the inline snippet: the origins and one `'sha256-…'` per inline script block. These hashes are this site's (its key is in the bootstrap's call) and change when the snippet does: re-read them after a banner or blocking change."
                        },
                        "connectSrc": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "The ruleset, consent decisions and tag events."
                        },
                        "imgSrc": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "The owner's logo on a paid Consent banner."
                        },
                        "styleSrc": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "The banner puts a <style> element in its shadow root, so style-src needs 'unsafe-inline'."
                        }
                      },
                      "required": [
                        "origins",
                        "siteTagScriptSrc",
                        "snippetScriptSrc",
                        "connectSrc",
                        "imgSrc",
                        "styleSrc"
                      ],
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "siteId",
                    "siteKey",
                    "region",
                    "snippet",
                    "deliveryOrigin",
                    "rulesetUrl",
                    "consentIngestUrl",
                    "monitoringIngestUrl",
                    "parts",
                    "ready",
                    "problems"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/status": {
      "get": {
        "operationId": "getSiteStatus",
        "summary": "Read a site's setup state",
        "description": "The whole setup state of one site in a single call: verified, ruleset published, tag manager connected, and whether either product has ever been heard from. Plus `nextAction` -- one sentence naming the most useful thing to do next.\n\nIt exists so an agent does not have to compose five reads and then work out what they mean. Read `nextAction` first and the booleans only if you disagree with it.\n\n`monitoringSignalSeen: false` IS NOT A FAILED INSTALL. Monitoring runs only after a consent decision that grants the category it rides on.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "sites:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read a site's setup state",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "siteId": {
                      "type": "string"
                    },
                    "domain": {
                      "type": "string"
                    },
                    "region": {
                      "type": "string",
                      "enum": [
                        "EU",
                        "US"
                      ]
                    },
                    "verified": {
                      "type": "boolean",
                      "description": "Has this account proved it controls the domain."
                    },
                    "rulesetPublished": {
                      "type": "boolean"
                    },
                    "tagManagerConnected": {
                      "type": "boolean"
                    },
                    "snippetInstalled": {
                      "type": "boolean",
                      "description": "Is the snippet on the site: a consent decision arrived, a scan or look read it on the homepage, or the domain was verified by its meta tag and the ruleset is served. False is not proof it is absent."
                    },
                    "consentSignalSeen": {
                      "type": "boolean",
                      "description": "Have we ever received a consent decision from this site."
                    },
                    "monitoringSignalSeen": {
                      "type": "boolean",
                      "description": "Have we ever received a tag event. FALSE IS NOT A FAILED INSTALL: monitoring runs only after a consent decision that grants the category it rides on, so a correct install whose visitors reject analytics reports false forever, correctly."
                    },
                    "nextAction": {
                      "type": "string",
                      "description": "One sentence: the single most useful thing to do next, or that nothing is needed."
                    }
                  },
                  "required": [
                    "siteId",
                    "domain",
                    "region",
                    "verified",
                    "rulesetPublished",
                    "tagManagerConnected",
                    "snippetInstalled",
                    "consentSignalSeen",
                    "monitoringSignalSeen",
                    "nextAction"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/verification": {
      "get": {
        "operationId": "getSiteVerification",
        "summary": "How to prove you own this domain",
        "description": "Whether this domain is proved, which route proved it (`method`), and the token for the two routes that need one: a DNS TXT record, or a meta tag in the site's <head>.\n\nUSUALLY NOTHING TO DO. Once this site's tag or snippet (`site_code`), or a Tag Manager container connected to it with edit access (`tag_manager`), is in the homepage's HTML, we find it ourselves: a background check reads the homepage every 10 minutes for the first hour after this site's code or token was first handed out, hourly for a day, then daily for 30 days. `methods` lists all four routes and which need a call.\n\nWHY THIS BLOCKS A BANNER. An unverified domain is refused before its configuration is even read, so it serves no banner to anyone. Serving one is a claim about a domain, and we do not make that claim for somebody who has not shown they control it.\n\nThe token is returned in clear and that is not a leak: it proves control by appearing where only the owner could put it. Knowing the string buys nothing.\n\nPrefer DNS for a domain whose zone you control -- it survives a redeploy. Prefer the meta tag when you want it done in one deploy, or when the zone is somebody else's. A tag injected only by JavaScript is not in the server's HTML, so it needs one of these two.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "sites:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "How to prove you own this domain",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "siteId": {
                      "type": "string"
                    },
                    "domain": {
                      "type": "string"
                    },
                    "verified": {
                      "type": "boolean"
                    },
                    "verifiedAt": {
                      "anyOf": [
                        {
                          "type": "string",
                          "description": "ISO-8601 timestamp, UTC."
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "method": {
                      "anyOf": [
                        {
                          "type": "string",
                          "enum": [
                            "dns_txt",
                            "meta_tag",
                            "site_code",
                            "tag_manager"
                          ]
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Which route proved it; null until one has."
                    },
                    "token": {
                      "type": "string"
                    },
                    "dnsTxt": {
                      "type": "object",
                      "properties": {
                        "names": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "value": {
                          "type": "string",
                          "description": "The exact TXT value to paste."
                        }
                      },
                      "required": [
                        "names",
                        "value"
                      ],
                      "additionalProperties": false,
                      "description": "The DNS route. Slower to propagate, and the one to prefer for a domain you control the zone for, because it survives a redeploy of the site."
                    },
                    "metaTag": {
                      "type": "object",
                      "properties": {
                        "html": {
                          "type": "string",
                          "description": "The exact line to paste into <head>."
                        },
                        "checkedUrl": {
                          "type": "string",
                          "description": "The page we fetch, over https, and nothing else."
                        }
                      },
                      "required": [
                        "html",
                        "checkedUrl"
                      ],
                      "additionalProperties": false,
                      "description": "The HTML route. Instant, and it needs a deploy."
                    },
                    "methods": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "method": {
                            "type": "string",
                            "enum": [
                              "dns_txt",
                              "meta_tag",
                              "site_code",
                              "tag_manager"
                            ]
                          },
                          "automatic": {
                            "type": "boolean",
                            "description": "True when installing is the whole proof: our background check reads the homepage and finds it, with no call from you."
                          },
                          "how": {
                            "type": "string",
                            "description": "What proves the domain by this route."
                          }
                        },
                        "required": [
                          "method",
                          "automatic",
                          "how"
                        ],
                        "additionalProperties": false
                      },
                      "description": "Every route that proves a domain, the automatic ones first."
                    }
                  },
                  "required": [
                    "siteId",
                    "domain",
                    "verified",
                    "verifiedAt",
                    "method",
                    "token",
                    "dnsTxt",
                    "metaTag",
                    "methods"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/verification/checks": {
      "post": {
        "operationId": "checkSiteVerification",
        "summary": "Check the record now",
        "description": "Looks now instead of waiting for the background check: DNS for the TXT record, then one read of the homepage, which proves the domain by this site's own tag or snippet (`site_code`), a connected Tag Manager container (`tag_manager`) or the meta tag. Records the attempt. A POST because it makes real DNS queries and fetches the site.\n\nYOU DO NOT NEED THIS FOR `site_code` OR `tag_manager`. Installing the code is the proof: the background check described on GET /verification finds it within about 10 minutes. Call this to know sooner, with `only: \"meta_tag\"` (the homepage read) after a deploy.\n\nEVERY OUTCOME IS A 200 WITH A STATUS, except a rate limit. `not_found` (the record is not published yet, or DNS has not propagated) and `unreachable` (we could not ask) have different next actions, and a caller polling this in a wizard has to tell 'keep waiting' from 'stop and fix something'.\n\nRate limited: 10 full checks per site per hour, and a 429 carries `retry-after` in seconds. A `dns_txt`-only check costs a sixth of a full one. DNS takes minutes to propagate, so polling faster cannot make the record appear sooner.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "sites:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "only": {
                    "description": "Check ONE mechanism. `dns_txt` makes no request to your site and costs a sixth of a full check against the hourly limit, so a poller waiting on DNS should use it. `meta_tag` is the homepage read, which also proves `site_code` and `tag_manager`, so use it after deploying the code. Omit for both.",
                    "type": "string",
                    "enum": [
                      "dns_txt",
                      "meta_tag"
                    ]
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Check the record now",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "verified",
                        "already_verified",
                        "not_found",
                        "unreachable",
                        "rate_limited"
                      ]
                    },
                    "verified": {
                      "type": "boolean"
                    },
                    "method": {
                      "anyOf": [
                        {
                          "type": "string",
                          "enum": [
                            "dns_txt",
                            "meta_tag",
                            "site_code",
                            "tag_manager"
                          ]
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Which route proved it: `site_code` (this site's tag or snippet on the homepage), `tag_manager` (a connected Tag Manager container loaded by it), `meta_tag` or `dns_txt`."
                    },
                    "detail": {
                      "type": "string",
                      "description": "One sentence a developer can act on."
                    },
                    "retryAfterSeconds": {
                      "anyOf": [
                        {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  },
                  "required": [
                    "status",
                    "verified",
                    "method",
                    "detail",
                    "retryAfterSeconds"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/scans": {
      "post": {
        "operationId": "startSiteScan",
        "summary": "Start a scan",
        "description": "Crawls the site in a real browser and records every tracker it finds.\n\nWHY THIS BLOCKS A BANNER TOO. A site no scan has finished serves none, because publishing a banner over trackers nobody has finished looking at would be a claim about an inventory we do not have. A site created through this API is in that state from birth.\n\n`started: false` IS SUCCESS: it means a scan was already queued or running and this request added nothing. Poll the scan rather than starting another.\n\n`scanId` is the tier-1 run this request started. Its `scan.completed` webhook says `tier: tier1`; a deeper tier-2 crawl follows a completed tier 1 and sends a second `scan.completed` with its own id and `tier: tier2`.\n\nThe site must be reachable over the public internet -- a crawl cannot see a password-protected staging environment or a localhost dev server.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "scans:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Start a scan",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "started": {
                      "type": "boolean",
                      "description": "False means a scan for this site was ALREADY queued or running and this request added nothing — not an error. Poll the scan instead of starting another."
                    },
                    "scanId": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The id of the tier-1 scan this request started: the `scanId` GET /sites/{siteId}/scans/latest returns for it and the `scanRunId` its `scan.completed` (tier: tier1) carries. Null when `started` is false."
                    },
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "started",
                    "scanId",
                    "detail"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/scans/latest": {
      "get": {
        "operationId": "getLatestSiteScan",
        "summary": "Has a scan finished yet",
        "description": "The latest run's progress, and — separately — whether this site's inventory is established at all.\n\nREAD `everSucceeded`, NOT `status`. `status` describes the latest RUN; `everSucceeded` describes the SITE, and it is the one that decides whether a banner can be served. A site whose most recent rescan failed but whose first scan completed still serves a banner; a site whose first scan is still running does not.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "sites:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Has a scan finished yet",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scanId": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The latest tier-1 run's id: the scan POST /scans starts, and the `scanRunId` a `scan.completed` webhook carries for it."
                    },
                    "tier": {
                      "type": "string",
                      "const": "tier1",
                      "description": "Always tier 1. A deeper tier-2 crawl can follow it; its own `scan.completed` says `tier: tier2`."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued",
                        "running",
                        "completed",
                        "partial",
                        "failed",
                        "none"
                      ],
                      "description": "The LATEST run. \"none\" means this site has never been scanned, and \"partial\" means the crawl budget ran out part-way — which does NOT establish the inventory, because \"found nothing\" would then cover only the pages it reached.\n\nRead `everSucceeded` for whether a banner can be served, not this."
                    },
                    "startedAt": {
                      "anyOf": [
                        {
                          "type": "string",
                          "description": "ISO-8601 timestamp, UTC."
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "finishedAt": {
                      "anyOf": [
                        {
                          "type": "string",
                          "description": "ISO-8601 timestamp, UTC."
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "everSucceeded": {
                      "type": "boolean",
                      "description": "THE FIELD THAT MATTERS. True when SOME tier-1 scan has reached `completed` and none is in flight — which is the compiler's own rule for whether this site's inventory is established, and therefore whether it can serve a banner at all. It is about the SITE, not the latest run: a later failed rescan does not un-see what a completed scan saw."
                    },
                    "detail": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "scanId",
                    "tier",
                    "status",
                    "startedAt",
                    "finishedAt",
                    "everSucceeded",
                    "detail"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/device-authorizations": {
      "post": {
        "operationId": "beginDeviceAuthorization",
        "summary": "Start a device authorization",
        "description": "How a terminal with no credential gets one. UNAUTHENTICATED, and the only operation on this surface that is.\n\nYou get a `userCode` to show the person and a `deviceCode` to keep. Send them to `verificationUrlComplete`, where they sign in -- creating an account if they have none -- and approve or refuse the request. Poll `POST /device-authorizations/token` with the device code until it answers.\n\nWHY THERE IS NO ENDPOINT THAT JUST CREATES AN ACCOUNT: minting an identity with nobody present means no verified email and no ceiling on abuse. A human in a browser is the control, and this grant is how a headless process borrows one.\n\nTHE KNOWN WEAKNESS, stated plainly because every implementation of this grant has it: an attacker can start a flow and talk somebody into approving it, and what they get is that person's own account. Do not send a user code to anyone. Our approval screen names the client and the directory that asked and tells a person who did not just run a CLI to press Deny.",
        "tags": [
          "Getting a key"
        ],
        "security": [],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "clientName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120,
                    "description": "How this client will name itself on the approval screen, e.g. \"tagsentry-cli 0.1.0 (darwin 24.0)\". Displayed to a human as text and trusted for nothing."
                  },
                  "projectHint": {
                    "description": "The directory or package name the client is running in, so a person with two terminals open can tell which one asked.",
                    "type": "string",
                    "maxLength": 120
                  },
                  "scopes": {
                    "minItems": 1,
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The scopes the issued key should carry. Restricted to what a terminal is allowed to be granted this way -- see lib/api/deviceAuthorization.ts. A request for anything outside that set is refused whole, so the approval screen can never be made to ask a human for a scope this flow cannot grant."
                  }
                },
                "required": [
                  "clientName",
                  "scopes"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Start a device authorization",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deviceCode": {
                      "type": "string",
                      "description": "Your secret. Send it to /device-authorizations/token. Never displayed to a human."
                    },
                    "userCode": {
                      "type": "string",
                      "description": "What the person reads off your output, e.g. \"BQ7K-F3XM\"."
                    },
                    "verificationUrl": {
                      "type": "string",
                      "description": "Where the human approves. Show this even when you also open a browser."
                    },
                    "verificationUrlComplete": {
                      "type": "string",
                      "description": "The same page with the code already filled in. This is the one to open."
                    },
                    "expiresAt": {
                      "type": "string",
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "expiresIn": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "Seconds until this authorization dies."
                    },
                    "interval": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "Seconds to wait between polls."
                    }
                  },
                  "required": [
                    "deviceCode",
                    "userCode",
                    "verificationUrl",
                    "verificationUrlComplete",
                    "expiresAt",
                    "expiresIn",
                    "interval"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/device-authorizations/token": {
      "post": {
        "operationId": "claimDeviceAuthorization",
        "summary": "Poll for the approved key",
        "description": "Exchange a device code for the key a human approved. UNAUTHENTICATED -- the device code IS the credential, which is why it must never be logged or displayed.\n\n`status: \"pending\"` with a 200 means nobody has answered yet: wait `interval` seconds and ask again. Anything else is a 4xx and means stop -- denied, expired, already claimed and unknown are each their own refusal, because the right next step differs for each.\n\nThe key is minted BY THIS CALL, not by the approval, so its secret exists for the first time in this response and is never stored in clear anywhere. Write it somewhere outside your repository.",
        "tags": [
          "Getting a key"
        ],
        "security": [],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "deviceCode": {
                    "type": "string",
                    "description": "The secret from POST /device-authorizations."
                  }
                },
                "required": [
                  "deviceCode"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Poll for the approved key",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "issued"
                      ],
                      "description": "\"pending\" means keep polling and is NOT an error. Every other outcome -- denied, expired, already claimed, unknown -- is a 4xx with a code, because each one means stop."
                    },
                    "apiKey": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "THE ONLY TIME THIS VALUE EXISTS OUTSIDE OUR MINT. It is not stored in clear and cannot be recovered; a lost key is re-minted, never recalled. Null while pending."
                    },
                    "accountId": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "prefix": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The key's visible prefix, for display and for logs."
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "What the human actually approved. May be empty while pending."
                    },
                    "expiresAt": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "When the issued key stops working (ISO 8601). Run `tagsentry login` for a new one. Null while pending."
                    }
                  },
                  "required": [
                    "status",
                    "apiKey",
                    "accountId",
                    "prefix",
                    "scopes",
                    "expiresAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The request conflicts with existing state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/banner/design": {
      "put": {
        "operationId": "updateBannerDesign",
        "summary": "Change the banner's colours and placement",
        "description": "Saves and publishes the banner's theme tokens and, optionally, where it sits. The same save the Design screen makes, with the same refusals: a theme whose text or buttons fail the contrast check is refused with the audit in `detail`, and nothing is stored.\n\nA DESIGN WRITE CANNOT CHANGE A WORD. Every string a visitor reads is carried from the stored banner, because each consent record quotes the banner verbatim. Use the wording endpoint for words.\n\nSaving IS publishing: visitors get the new design on their next page view. The site's colours are marked as yours from here, so a later scan does not re-derive over them.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "banner:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "theme": {
                    "type": "object",
                    "propertyNames": {
                      "type": "string",
                      "enum": [
                        "--ts-panel-bg",
                        "--ts-panel-fg",
                        "--ts-panel-border",
                        "--ts-panel-radius",
                        "--ts-body-fg",
                        "--ts-muted-fg",
                        "--ts-button-bg",
                        "--ts-button-fg",
                        "--ts-button-radius",
                        "--ts-divider",
                        "--ts-focus-ring",
                        "--ts-font"
                      ]
                    },
                    "additionalProperties": {
                      "type": "string",
                      "maxLength": 256
                    },
                    "description": "CSS custom properties. An absent token keeps our shipped default. Refused if a contrast check fails."
                  },
                  "presentation": {
                    "description": "Absent keeps the stored placement. `{}` resets it to where we ship it.",
                    "type": "object",
                    "properties": {
                      "layout": {
                        "type": "string",
                        "enum": [
                          "bar_bottom",
                          "bar_top",
                          "box_bottom_left",
                          "box_bottom_right",
                          "box_top_left",
                          "box_top_right",
                          "modal_centre"
                        ]
                      },
                      "overlay": {
                        "description": "The scrim behind a first-visit banner. Off also drops the focus trap.",
                        "type": "boolean"
                      },
                      "actions": {
                        "type": "string",
                        "enum": [
                          "inline",
                          "stacked"
                        ]
                      },
                      "order": {
                        "type": "string",
                        "enum": [
                          "reject_first",
                          "accept_first"
                        ]
                      },
                      "closeX": {
                        "type": "string",
                        "enum": [
                          "retains_defaults"
                        ]
                      },
                      "launcher": {
                        "type": "object",
                        "properties": {
                          "position": {
                            "type": "string",
                            "enum": [
                              "bottom_left",
                              "bottom_right",
                              "top_left",
                              "top_right"
                            ]
                          },
                          "trigger": {
                            "description": "A CSS selector for your own 'Cookie settings' control. Falls back to the floating pill when it matches nothing.",
                            "type": "string",
                            "maxLength": 512
                          }
                        },
                        "additionalProperties": false
                      }
                    },
                    "additionalProperties": false
                  }
                },
                "required": [
                  "theme"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Change the banner's colours and placement",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saved": {
                      "type": "boolean",
                      "const": true
                    },
                    "change": {
                      "type": "string",
                      "enum": [
                        "design",
                        "wording",
                        "links"
                      ]
                    },
                    "banner": {
                      "type": "object",
                      "properties": {
                        "title": {
                          "type": "string"
                        },
                        "body": {
                          "type": "string"
                        },
                        "acceptAll": {
                          "type": "string"
                        },
                        "rejectAll": {
                          "type": "string"
                        },
                        "manage": {
                          "type": "string"
                        },
                        "theme": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {
                                "type": "string"
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "presentation": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {}
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "policyLinks": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {
                                "type": "string"
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      },
                      "required": [
                        "title",
                        "body",
                        "acceptAll",
                        "rejectAll",
                        "manage",
                        "theme",
                        "presentation",
                        "policyLinks"
                      ],
                      "additionalProperties": false
                    },
                    "designMode": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "artifactWarning": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Set when the save stored but the copy visitors fetch did not refresh yet. It retries; the save stands."
                    }
                  },
                  "required": [
                    "saved",
                    "change",
                    "banner",
                    "designMode",
                    "artifactWarning"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The request conflicts with existing state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/banner/wording": {
      "put": {
        "operationId": "updateBannerWording",
        "summary": "Change the banner's words",
        "description": "Saves and publishes the heading, the explanation, the three button labels and, optionally, each purpose's name and description. All five texts are required; to change some of them, PATCH the same path. Colours and placement are carried from the stored banner. Blank text is refused: a visitor would see an unlabelled control.\n\nConsent records made after this save quote the new words; records made before keep the words they were given.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "banner:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "body": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "acceptAll": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "rejectAll": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "manage": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "categoryCopy": {
                    "description": "Per purpose. Only purposes the site already has are honoured; an omitted one keeps its copy.",
                    "type": "object",
                    "propertyNames": {
                      "type": "string"
                    },
                    "additionalProperties": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string"
                        }
                      },
                      "additionalProperties": false
                    }
                  }
                },
                "required": [
                  "title",
                  "body",
                  "acceptAll",
                  "rejectAll",
                  "manage"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Change the banner's words",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saved": {
                      "type": "boolean",
                      "const": true
                    },
                    "change": {
                      "type": "string",
                      "enum": [
                        "design",
                        "wording",
                        "links"
                      ]
                    },
                    "banner": {
                      "type": "object",
                      "properties": {
                        "title": {
                          "type": "string"
                        },
                        "body": {
                          "type": "string"
                        },
                        "acceptAll": {
                          "type": "string"
                        },
                        "rejectAll": {
                          "type": "string"
                        },
                        "manage": {
                          "type": "string"
                        },
                        "theme": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {
                                "type": "string"
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "presentation": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {}
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "policyLinks": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {
                                "type": "string"
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      },
                      "required": [
                        "title",
                        "body",
                        "acceptAll",
                        "rejectAll",
                        "manage",
                        "theme",
                        "presentation",
                        "policyLinks"
                      ],
                      "additionalProperties": false
                    },
                    "designMode": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "artifactWarning": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Set when the save stored but the copy visitors fetch did not refresh yet. It retries; the save stands."
                    }
                  },
                  "required": [
                    "saved",
                    "change",
                    "banner",
                    "designMode",
                    "artifactWarning"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The request conflicts with existing state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getBannerWording",
        "summary": "Read the banner's words",
        "description": "The heading, the explanation, the three button labels and each purpose's name and description, as visitors read them now. The starting point for a PUT or PATCH.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "sites:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read the banner's words",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "title": {
                      "type": "string"
                    },
                    "body": {
                      "type": "string"
                    },
                    "acceptAll": {
                      "type": "string"
                    },
                    "rejectAll": {
                      "type": "string"
                    },
                    "manage": {
                      "type": "string"
                    },
                    "categoryCopy": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "name",
                          "description"
                        ],
                        "additionalProperties": false
                      },
                      "description": "Each purpose the site has, with the name and description the preferences view shows."
                    }
                  },
                  "required": [
                    "title",
                    "body",
                    "acceptAll",
                    "rejectAll",
                    "manage",
                    "categoryCopy"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The request conflicts with existing state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "patchBannerWording",
        "summary": "Change some of the banner's words",
        "description": "Any subset of the wording, merged over what is stored: send only `acceptAll` to relabel one button. Every other rule is PUT's: blank text is refused, colours and placement are carried, and saving is publishing.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "banner:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "body": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "acceptAll": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "rejectAll": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "manage": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000
                  },
                  "categoryCopy": {
                    "description": "Per purpose. Only purposes the site already has are honoured; an omitted one keeps its copy.",
                    "type": "object",
                    "propertyNames": {
                      "type": "string"
                    },
                    "additionalProperties": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string"
                        }
                      },
                      "additionalProperties": false
                    }
                  }
                },
                "additionalProperties": false,
                "description": "Any subset of the wording. An omitted field keeps its stored text."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Change some of the banner's words",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saved": {
                      "type": "boolean",
                      "const": true
                    },
                    "change": {
                      "type": "string",
                      "enum": [
                        "design",
                        "wording",
                        "links"
                      ]
                    },
                    "banner": {
                      "type": "object",
                      "properties": {
                        "title": {
                          "type": "string"
                        },
                        "body": {
                          "type": "string"
                        },
                        "acceptAll": {
                          "type": "string"
                        },
                        "rejectAll": {
                          "type": "string"
                        },
                        "manage": {
                          "type": "string"
                        },
                        "theme": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {
                                "type": "string"
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "presentation": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {}
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "policyLinks": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {
                                "type": "string"
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      },
                      "required": [
                        "title",
                        "body",
                        "acceptAll",
                        "rejectAll",
                        "manage",
                        "theme",
                        "presentation",
                        "policyLinks"
                      ],
                      "additionalProperties": false
                    },
                    "designMode": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "artifactWarning": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Set when the save stored but the copy visitors fetch did not refresh yet. It retries; the save stands."
                    }
                  },
                  "required": [
                    "saved",
                    "change",
                    "banner",
                    "designMode",
                    "artifactWarning"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The request conflicts with existing state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/banner/links": {
      "put": {
        "operationId": "updateBannerLinks",
        "summary": "Change the banner's policy links",
        "description": "Replaces the privacy policy, cookie policy and imprint links. Send every link you want kept: an omitted or empty one is cleared. Only http(s) URLs are accepted.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "banner:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "privacyPolicy": {
                    "type": "string",
                    "maxLength": 2048
                  },
                  "cookiePolicy": {
                    "type": "string",
                    "maxLength": 2048
                  },
                  "imprint": {
                    "type": "string",
                    "maxLength": 2048
                  }
                },
                "additionalProperties": false,
                "description": "An empty string clears that link. An omitted one is cleared too: send all three you want kept."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Change the banner's policy links",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saved": {
                      "type": "boolean",
                      "const": true
                    },
                    "change": {
                      "type": "string",
                      "enum": [
                        "design",
                        "wording",
                        "links"
                      ]
                    },
                    "banner": {
                      "type": "object",
                      "properties": {
                        "title": {
                          "type": "string"
                        },
                        "body": {
                          "type": "string"
                        },
                        "acceptAll": {
                          "type": "string"
                        },
                        "rejectAll": {
                          "type": "string"
                        },
                        "manage": {
                          "type": "string"
                        },
                        "theme": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {
                                "type": "string"
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "presentation": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {}
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "policyLinks": {
                          "anyOf": [
                            {
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {
                                "type": "string"
                              }
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      },
                      "required": [
                        "title",
                        "body",
                        "acceptAll",
                        "rejectAll",
                        "manage",
                        "theme",
                        "presentation",
                        "policyLinks"
                      ],
                      "additionalProperties": false
                    },
                    "designMode": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "artifactWarning": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Set when the save stored but the copy visitors fetch did not refresh yet. It retries; the save stands."
                    }
                  },
                  "required": [
                    "saved",
                    "change",
                    "banner",
                    "designMode",
                    "artifactWarning"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "409": {
            "description": "The request conflicts with existing state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/consent-export": {
      "get": {
        "operationId": "exportConsentRecords",
        "summary": "Download the proof-of-consent export",
        "description": "The regulator-facing document: every consent record in scope, oldest first, with its limitations and a completeness statement. The same document the dashboard downloads.\n\nREAD `completeness` FIRST. Past its record cap the document keeps the newest and says exactly how many it left out; export in `from`/`to` windows to get the rest. The `X-Consent-Export-Complete` header carries the same fact.\n\nOnly for a verified domain: otherwise 403 `domain_not_verified`.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "consent:export"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "ISO-8601 lower bound on receivedAt.",
            "schema": {
              "description": "ISO-8601 lower bound on receivedAt.",
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "ISO-8601 upper bound on receivedAt.",
            "schema": {
              "description": "ISO-8601 upper bound on receivedAt.",
              "type": "string"
            }
          },
          {
            "name": "subjectId",
            "in": "query",
            "required": false,
            "description": "One device's history.",
            "schema": {
              "description": "One device's history.",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Download the proof-of-consent export",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exportVersion": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991
                    },
                    "generatedAt": {
                      "type": "string",
                      "description": "ISO-8601 timestamp, UTC."
                    },
                    "scope": {
                      "type": "object",
                      "properties": {
                        "siteId": {
                          "type": "string"
                        },
                        "subjectId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      },
                      "required": [
                        "siteId",
                        "subjectId"
                      ],
                      "additionalProperties": false
                    },
                    "history": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "propertyNames": {
                          "type": "string"
                        },
                        "additionalProperties": {}
                      }
                    },
                    "current": {
                      "anyOf": [
                        {
                          "type": "object",
                          "propertyNames": {
                            "type": "string"
                          },
                          "additionalProperties": {}
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "limitations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "propertyNames": {
                          "type": "string"
                        },
                        "additionalProperties": {}
                      }
                    },
                    "completeness": {
                      "type": "object",
                      "properties": {
                        "complete": {
                          "type": "boolean"
                        },
                        "recordsInScope": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        "recordsOmitted": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        }
                      },
                      "required": [
                        "complete",
                        "recordsInScope",
                        "recordsOmitted"
                      ],
                      "additionalProperties": {},
                      "description": "READ THIS FIRST. `complete: false` means the document is partial and says by how much."
                    }
                  },
                  "required": [
                    "exportVersion",
                    "generatedAt",
                    "scope",
                    "history",
                    "current",
                    "limitations",
                    "completeness"
                  ],
                  "additionalProperties": {}
                }
              }
            }
          },
          "400": {
            "description": "The request did not validate against this operation's schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/trackers": {
      "get": {
        "operationId": "listSiteTrackers",
        "summary": "Read the tracker inventory",
        "description": "Every tracker our scans and the connected Tag Manager container found, grouped by vendor, with our category for each tag and the cookies each vendor set.\n\n`category` is our answer: from our sourced vendor table where it has one, else from our classifier. `needsReview` means no human has confirmed it, not that nobody decided.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "trackers:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read the tracker inventory",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "vendorName": {
                            "type": "string"
                          },
                          "vendorHost": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "vendorIsRecognised": {
                            "type": "boolean"
                          },
                          "vendorBestGuess": {
                            "anyOf": [
                              {
                                "type": "object",
                                "properties": {
                                  "vendor": {
                                    "type": "string"
                                  },
                                  "product": {
                                    "type": "string"
                                  },
                                  "category": {
                                    "type": "string",
                                    "enum": [
                                      "analytics",
                                      "advertising",
                                      "functional",
                                      "necessary",
                                      "unknown"
                                    ]
                                  },
                                  "whatItDoes": {
                                    "type": "string"
                                  },
                                  "confidence": {
                                    "type": "number"
                                  }
                                },
                                "required": [
                                  "vendor",
                                  "product",
                                  "category",
                                  "whatItDoes",
                                  "confidence"
                                ],
                                "additionalProperties": false
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "Our best guess at who an unrecognised host is, from its address alone; vendorName is then this vendor. Nobody has confirmed it. Null when the host is recognised or we have no guess."
                          },
                          "sources": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "gtm_container",
                                "live_scan",
                                "both"
                              ]
                            }
                          },
                          "unmanagedCount": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "Tags seen on the page that no Tag Manager container we can read fires."
                          },
                          "needsReviewCount": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "No human has confirmed these. We still decided a category where we could."
                          },
                          "lastSeenAt": {
                            "type": "string",
                            "description": "ISO-8601 timestamp, UTC."
                          },
                          "tags": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string"
                                },
                                "displayName": {
                                  "type": "string"
                                },
                                "source": {
                                  "type": "string",
                                  "enum": [
                                    "gtm_container",
                                    "live_scan",
                                    "both"
                                  ]
                                },
                                "gtmContainerPublicId": {
                                  "anyOf": [
                                    {
                                      "type": "string"
                                    },
                                    {
                                      "type": "null"
                                    }
                                  ]
                                },
                                "gtmTagId": {
                                  "anyOf": [
                                    {
                                      "type": "string"
                                    },
                                    {
                                      "type": "null"
                                    }
                                  ]
                                },
                                "gtmTagType": {
                                  "anyOf": [
                                    {
                                      "type": "string"
                                    },
                                    {
                                      "type": "null"
                                    }
                                  ]
                                },
                                "isUnmanaged": {
                                  "type": "boolean"
                                },
                                "needsReview": {
                                  "type": "boolean"
                                },
                                "category": {
                                  "anyOf": [
                                    {
                                      "type": "string"
                                    },
                                    {
                                      "type": "null"
                                    }
                                  ],
                                  "description": "Our answer, or null when neither source has one."
                                },
                                "categoryBasis": {
                                  "anyOf": [
                                    {
                                      "type": "string",
                                      "enum": [
                                        "vendor_table",
                                        "classifier"
                                      ]
                                    },
                                    {
                                      "type": "null"
                                    }
                                  ]
                                },
                                "firstSeenAt": {
                                  "type": "string",
                                  "description": "ISO-8601 timestamp, UTC."
                                },
                                "lastSeenAt": {
                                  "type": "string",
                                  "description": "ISO-8601 timestamp, UTC."
                                }
                              },
                              "required": [
                                "id",
                                "displayName",
                                "source",
                                "gtmContainerPublicId",
                                "gtmTagId",
                                "gtmTagType",
                                "isUnmanaged",
                                "needsReview",
                                "category",
                                "categoryBasis",
                                "firstSeenAt",
                                "lastSeenAt"
                              ],
                              "additionalProperties": false
                            }
                          },
                          "cookies": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "domain": {
                                  "type": "string"
                                },
                                "party": {
                                  "type": "string",
                                  "enum": [
                                    "first",
                                    "third"
                                  ]
                                },
                                "expiry": {
                                  "anyOf": [
                                    {
                                      "type": "string",
                                      "const": "session"
                                    },
                                    {
                                      "type": "number"
                                    },
                                    {
                                      "type": "null"
                                    }
                                  ],
                                  "description": "Seconds, 'session', or null when unknown."
                                },
                                "setBy": {
                                  "anyOf": [
                                    {
                                      "type": "object",
                                      "properties": {
                                        "via": {
                                          "type": "string",
                                          "enum": [
                                            "http",
                                            "script",
                                            "domain"
                                          ]
                                        },
                                        "host": {
                                          "anyOf": [
                                            {
                                              "type": "string"
                                            },
                                            {
                                              "type": "null"
                                            }
                                          ]
                                        }
                                      },
                                      "required": [
                                        "via",
                                        "host"
                                      ],
                                      "additionalProperties": false
                                    },
                                    {
                                      "type": "null"
                                    }
                                  ]
                                }
                              },
                              "required": [
                                "name",
                                "domain",
                                "party",
                                "expiry",
                                "setBy"
                              ],
                              "additionalProperties": false
                            }
                          }
                        },
                        "required": [
                          "vendorName",
                          "vendorHost",
                          "vendorIsRecognised",
                          "vendorBestGuess",
                          "sources",
                          "unmanagedCount",
                          "needsReviewCount",
                          "lastSeenAt",
                          "tags",
                          "cookies"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "limit": {
                      "type": "integer",
                      "minimum": -9007199254740991,
                      "maximum": 9007199254740991,
                      "description": "The row cap applied to the underlying read."
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "True when the read came back at its cap. When true, this page is NOT the whole answer -- narrow the window. The tracker inventory is read whole; `truncated` is always false here."
                    },
                    "truncationHint": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "data",
                    "limit",
                    "truncated"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/billing": {
      "get": {
        "operationId": "getBilling",
        "summary": "Read each site's tiers and usage",
        "description": "Per active site: its tier in each product (Consent and Event monitoring, each Free or Pro), what that costs a month, and this month's metered usage against its allowance. `usage.countedThrough` is where the count stops; later events are not in it yet.\n\nPayment state never changes what a banner serves. Owner-minted keys only. A site-pinned key sees its own site and no account payment state.",
        "tags": [
          "Account"
        ],
        "security": [
          {
            "apiKey": [
              "billing:read"
            ]
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Read each site's tiers and usage",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "paymentProblem": {
                      "anyOf": [
                        {
                          "type": "object",
                          "properties": {
                            "severity": {
                              "type": "string"
                            },
                            "headline": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "severity",
                            "headline"
                          ],
                          "additionalProperties": false
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Null when Stripe has reported nothing wrong. Never changes what a banner serves."
                    },
                    "sites": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "siteId": {
                            "type": "string"
                          },
                          "domain": {
                            "type": "string"
                          },
                          "tiers": {
                            "type": "object",
                            "properties": {
                              "consent": {
                                "type": "string",
                                "enum": [
                                  "free",
                                  "paid"
                                ]
                              },
                              "monitoring": {
                                "type": "string",
                                "enum": [
                                  "free",
                                  "paid"
                                ]
                              }
                            },
                            "required": [
                              "consent",
                              "monitoring"
                            ],
                            "additionalProperties": false
                          },
                          "monthlyPriceCents": {
                            "type": "integer",
                            "minimum": -9007199254740991,
                            "maximum": 9007199254740991,
                            "description": "What this site's two products cost a month, before usage."
                          },
                          "awaitingStripeReconciliation": {
                            "type": "boolean"
                          },
                          "usage": {
                            "type": "object",
                            "properties": {
                              "billingMonth": {
                                "type": "string"
                              },
                              "countedThrough": {
                                "anyOf": [
                                  {
                                    "type": "string",
                                    "description": "ISO-8601 timestamp, UTC."
                                  },
                                  {
                                    "type": "null"
                                  }
                                ],
                                "description": "The end of the last counted window. Later events are not in these figures yet."
                              },
                              "hasUnconfirmedWindows": {
                                "type": "boolean"
                              },
                              "byDimension": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "dimension": {
                                      "type": "string"
                                    },
                                    "product": {
                                      "type": "string"
                                    },
                                    "tier": {
                                      "type": "string"
                                    },
                                    "quantity": {
                                      "type": "integer",
                                      "minimum": -9007199254740991,
                                      "maximum": 9007199254740991
                                    },
                                    "included": {
                                      "anyOf": [
                                        {
                                          "type": "integer",
                                          "minimum": -9007199254740991,
                                          "maximum": 9007199254740991
                                        },
                                        {
                                          "type": "null"
                                        }
                                      ]
                                    },
                                    "overQuantity": {
                                      "type": "integer",
                                      "minimum": -9007199254740991,
                                      "maximum": 9007199254740991
                                    }
                                  },
                                  "required": [
                                    "dimension",
                                    "product",
                                    "tier",
                                    "quantity",
                                    "included",
                                    "overQuantity"
                                  ],
                                  "additionalProperties": false
                                }
                              }
                            },
                            "required": [
                              "billingMonth",
                              "countedThrough",
                              "hasUnconfirmedWindows",
                              "byDimension"
                            ],
                            "additionalProperties": false
                          }
                        },
                        "required": [
                          "siteId",
                          "domain",
                          "tiers",
                          "monthlyPriceCents",
                          "awaitingStripeReconciliation",
                          "usage"
                        ],
                        "additionalProperties": false
                      }
                    }
                  },
                  "required": [
                    "paymentProblem",
                    "sites"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{siteId}/gtm": {
      "get": {
        "operationId": "getSiteGtmContainer",
        "summary": "Read the connected Tag Manager inventory",
        "description": "The containers connected to this site, whether we can write to each, and the container's tags as our last scan read them. Served from what we stored, never a live call to Google: the Tag Manager API quota is shared by every customer.\n\nREAD ONLY. This version of the API has no Tag Manager write: `gtm:write` is a scope with no endpoint yet. Container changes go through the dashboard, where a person approves them.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": [
              "gtm:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "siteId",
            "in": "path",
            "required": true,
            "description": "The site id, from GET /v1/sites.",
            "schema": {
              "type": "string",
              "description": "The site id, from GET /v1/sites."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read the connected Tag Manager inventory",
            "headers": {
              "X-Request-Id": {
                "description": "Echoed in the body of any error. Quote it when reporting a problem.",
                "schema": {
                  "type": "string"
                }
              },
              "X-TagSentry-Region": {
                "description": "Which regional database served this response, when it read regional data. A site's region is fixed at creation and is never taken from the request.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "EU",
                    "US"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "containers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "connectionId": {
                            "type": "string"
                          },
                          "gtmContainerPublicId": {
                            "type": "string"
                          },
                          "containerName": {
                            "type": "string"
                          },
                          "state": {
                            "type": "string"
                          },
                          "accessLevel": {
                            "type": "string"
                          },
                          "canWrite": {
                            "type": "boolean"
                          },
                          "readAt": {
                            "anyOf": [
                              {
                                "type": "string",
                                "description": "ISO-8601 timestamp, UTC."
                              },
                              {
                                "type": "null"
                              }
                            ],
                            "description": "When our last scan read this container. Null if never."
                          }
                        },
                        "required": [
                          "connectionId",
                          "gtmContainerPublicId",
                          "containerName",
                          "state",
                          "accessLevel",
                          "canWrite",
                          "readAt"
                        ],
                        "additionalProperties": false
                      },
                      "description": "One entry per live container, the one shown first first. Disconnected containers are left out."
                    },
                    "primary": {
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The public id of the container shown first, or null. Kept for clients written before `containers`."
                    },
                    "connections": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "isPrimary": {
                            "type": "boolean"
                          },
                          "state": {
                            "type": "string"
                          },
                          "containerName": {
                            "type": "string"
                          },
                          "gtmContainerPublicId": {
                            "type": "string"
                          },
                          "accessLevel": {
                            "type": "string"
                          },
                          "canWrite": {
                            "type": "boolean"
                          },
                          "lastScanAt": {
                            "anyOf": [
                              {
                                "type": "string",
                                "description": "ISO-8601 timestamp, UTC."
                              },
                              {
                                "type": "null"
                              }
                            ]
                          }
                        },
                        "required": [
                          "id",
                          "isPrimary",
                          "state",
                          "containerName",
                          "gtmContainerPublicId",
                          "accessLevel",
                          "canWrite",
                          "lastScanAt"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "displayName": {
                            "type": "string"
                          },
                          "gtmContainerPublicId": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "gtmTagId": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "gtmTagType": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "vendorDomain": {
                            "anyOf": [
                              {
                                "type": "string"
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "lastSeenAt": {
                            "type": "string",
                            "description": "ISO-8601 timestamp, UTC."
                          }
                        },
                        "required": [
                          "id",
                          "displayName",
                          "gtmContainerPublicId",
                          "gtmTagId",
                          "gtmTagType",
                          "vendorDomain",
                          "lastSeenAt"
                        ],
                        "additionalProperties": false
                      },
                      "description": "The container's tags as our last scan read them, not a live read of Google."
                    },
                    "readAt": {
                      "anyOf": [
                        {
                          "type": "string",
                          "description": "ISO-8601 timestamp, UTC."
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "When a scan last read the primary container. Null if never."
                    }
                  },
                  "required": [
                    "containers",
                    "primary",
                    "connections",
                    "tags",
                    "readAt"
                  ],
                  "additionalProperties": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, unknown, revoked or expired API key. These are deliberately indistinguishable in the response -- distinguishing them would confirm to a caller that a token was once real.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "403": {
            "description": "The key authenticated but does not carry the scope(s) this operation requires, or (`domain_not_verified`) the site's domain is not verified, so its consent records are not released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "No such resource on this account. A site id belonging to a DIFFERENT account answers 404, never 403 -- a 403 would confirm the id exists somewhere.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "429": {
            "description": "A rate limit or quota was exceeded. The body names WHICH one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on our side. The requestId in the body is what to quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "consent.recorded": {
      "post": {
        "operationId": "webhook_consent_recorded",
        "summary": "Consent decisions were recorded",
        "description": "Batched: at most one POST a minute per site, covering every record since the last one. Counts and record ids, never visitor data.\n\n**Verifying a delivery.** Every POST carries `X-TagSentry-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256, keyed with the endpoint's secret (`whsec_…`, shown once when the endpoint is added), of the string `t + \".\" + rawBody`: the timestamp, a full stop, then the body EXACTLY as received, before any JSON parsing. Compare in constant time and refuse a `t` more than 300 seconds from your clock.\n\n```js\nconst crypto = require(\"node:crypto\");\nfunction verify(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {\n  const parts = Object.fromEntries(header.split(\",\").map((p) => p.split(\"=\")));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(nowSeconds - t) > 300) return false;\n  const expected = crypto.createHmac(\"sha256\", secret).update(`${t}.${rawBody}`).digest();\n  const given = Buffer.from(parts.v1 ?? \"\", \"hex\");\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n```\n\n`X-TagSentry-Event` names the event; `X-TagSentry-Delivery` is the delivery id, stable across retries. Answer 2xx within 10 seconds. A timeout, 408, 429 or 5xx is retried with backoff, 8 tries in all; any other answer, and a redirect, fails at once. We never follow redirects.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "X-TagSentry-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256 of \"t.body\">`, keyed with the endpoint's secret.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-TagSentry-Event",
            "in": "header",
            "required": true,
            "description": "The event type; equal to the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "consent.recorded"
              ]
            }
          },
          {
            "name": "X-TagSentry-Delivery",
            "in": "header",
            "required": true,
            "description": "The delivery id; equal to the body's `id`. Stable across retries.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The delivery id. Stable across retries and equal to `X-TagSentry-Delivery`: dedupe on it."
                  },
                  "type": {
                    "type": "string",
                    "const": "consent.recorded"
                  },
                  "version": {
                    "type": "integer",
                    "minimum": -9007199254740991,
                    "maximum": 9007199254740991,
                    "description": "Moves only when a receiver could notice. A new optional field does not move it."
                  },
                  "createdAt": {
                    "type": "string",
                    "description": "ISO-8601 timestamp, UTC."
                  },
                  "accountId": {
                    "type": "string"
                  },
                  "site": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "domain": {
                        "type": "string"
                      },
                      "region": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "id",
                      "domain",
                      "region"
                    ],
                    "additionalProperties": false
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "after": {
                        "anyOf": [
                          {
                            "type": "string",
                            "description": "ISO-8601 timestamp, UTC."
                          },
                          {
                            "type": "null"
                          }
                        ],
                        "description": "Records strictly after this were counted. Null on an endpoint's first batch."
                      },
                      "through": {
                        "type": "string",
                        "description": "Records up to and including this were counted."
                      },
                      "total": {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      },
                      "byMethod": {
                        "type": "object",
                        "propertyNames": {
                          "type": "string"
                        },
                        "additionalProperties": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        }
                      },
                      "recordIds": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "Oldest first, capped. Fetch a record with GET /sites/{siteId}/consent-records."
                      },
                      "recordIdsTruncated": {
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "after",
                      "through",
                      "total",
                      "byMethod",
                      "recordIds",
                      "recordIdsTruncated"
                    ],
                    "additionalProperties": false
                  }
                },
                "required": [
                  "id",
                  "type",
                  "version",
                  "createdAt",
                  "accountId",
                  "site",
                  "data"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is a failed attempt."
          }
        }
      }
    },
    "ruleset.published": {
      "post": {
        "operationId": "webhook_ruleset_published",
        "summary": "The banner's rules went live",
        "description": "A new ruleset became the one visitors are served.\n\n**Verifying a delivery.** Every POST carries `X-TagSentry-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256, keyed with the endpoint's secret (`whsec_…`, shown once when the endpoint is added), of the string `t + \".\" + rawBody`: the timestamp, a full stop, then the body EXACTLY as received, before any JSON parsing. Compare in constant time and refuse a `t` more than 300 seconds from your clock.\n\n```js\nconst crypto = require(\"node:crypto\");\nfunction verify(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {\n  const parts = Object.fromEntries(header.split(\",\").map((p) => p.split(\"=\")));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(nowSeconds - t) > 300) return false;\n  const expected = crypto.createHmac(\"sha256\", secret).update(`${t}.${rawBody}`).digest();\n  const given = Buffer.from(parts.v1 ?? \"\", \"hex\");\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n```\n\n`X-TagSentry-Event` names the event; `X-TagSentry-Delivery` is the delivery id, stable across retries. Answer 2xx within 10 seconds. A timeout, 408, 429 or 5xx is retried with backoff, 8 tries in all; any other answer, and a redirect, fails at once. We never follow redirects.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "X-TagSentry-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256 of \"t.body\">`, keyed with the endpoint's secret.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-TagSentry-Event",
            "in": "header",
            "required": true,
            "description": "The event type; equal to the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "ruleset.published"
              ]
            }
          },
          {
            "name": "X-TagSentry-Delivery",
            "in": "header",
            "required": true,
            "description": "The delivery id; equal to the body's `id`. Stable across retries.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The delivery id. Stable across retries and equal to `X-TagSentry-Delivery`: dedupe on it."
                  },
                  "type": {
                    "type": "string",
                    "const": "ruleset.published"
                  },
                  "version": {
                    "type": "integer",
                    "minimum": -9007199254740991,
                    "maximum": 9007199254740991,
                    "description": "Moves only when a receiver could notice. A new optional field does not move it."
                  },
                  "createdAt": {
                    "type": "string",
                    "description": "ISO-8601 timestamp, UTC."
                  },
                  "accountId": {
                    "type": "string"
                  },
                  "site": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "domain": {
                        "type": "string"
                      },
                      "region": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "id",
                      "domain",
                      "region"
                    ],
                    "additionalProperties": false
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "integrity": {
                        "type": "string"
                      },
                      "publishedAt": {
                        "type": "string",
                        "description": "ISO-8601 timestamp, UTC."
                      }
                    },
                    "required": [
                      "integrity",
                      "publishedAt"
                    ],
                    "additionalProperties": false
                  }
                },
                "required": [
                  "id",
                  "type",
                  "version",
                  "createdAt",
                  "accountId",
                  "site",
                  "data"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is a failed attempt."
          }
        }
      }
    },
    "scan.completed": {
      "post": {
        "operationId": "webhook_scan_completed",
        "summary": "A scan finished",
        "description": "Once per scan run, whatever its outcome. Read `status`: a failed scan is an event too.\n\n**Verifying a delivery.** Every POST carries `X-TagSentry-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256, keyed with the endpoint's secret (`whsec_…`, shown once when the endpoint is added), of the string `t + \".\" + rawBody`: the timestamp, a full stop, then the body EXACTLY as received, before any JSON parsing. Compare in constant time and refuse a `t` more than 300 seconds from your clock.\n\n```js\nconst crypto = require(\"node:crypto\");\nfunction verify(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {\n  const parts = Object.fromEntries(header.split(\",\").map((p) => p.split(\"=\")));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(nowSeconds - t) > 300) return false;\n  const expected = crypto.createHmac(\"sha256\", secret).update(`${t}.${rawBody}`).digest();\n  const given = Buffer.from(parts.v1 ?? \"\", \"hex\");\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n```\n\n`X-TagSentry-Event` names the event; `X-TagSentry-Delivery` is the delivery id, stable across retries. Answer 2xx within 10 seconds. A timeout, 408, 429 or 5xx is retried with backoff, 8 tries in all; any other answer, and a redirect, fails at once. We never follow redirects.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "X-TagSentry-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256 of \"t.body\">`, keyed with the endpoint's secret.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-TagSentry-Event",
            "in": "header",
            "required": true,
            "description": "The event type; equal to the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "scan.completed"
              ]
            }
          },
          {
            "name": "X-TagSentry-Delivery",
            "in": "header",
            "required": true,
            "description": "The delivery id; equal to the body's `id`. Stable across retries.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The delivery id. Stable across retries and equal to `X-TagSentry-Delivery`: dedupe on it."
                  },
                  "type": {
                    "type": "string",
                    "const": "scan.completed"
                  },
                  "version": {
                    "type": "integer",
                    "minimum": -9007199254740991,
                    "maximum": 9007199254740991,
                    "description": "Moves only when a receiver could notice. A new optional field does not move it."
                  },
                  "createdAt": {
                    "type": "string",
                    "description": "ISO-8601 timestamp, UTC."
                  },
                  "accountId": {
                    "type": "string"
                  },
                  "site": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "domain": {
                        "type": "string"
                      },
                      "region": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "id",
                      "domain",
                      "region"
                    ],
                    "additionalProperties": false
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "scanRunId": {
                        "type": "string",
                        "description": "The scan run's id: the same id GET /sites/{siteId}/scans/latest returns as `scanId` for that tier."
                      },
                      "tier": {
                        "type": "string",
                        "enum": [
                          "tier1",
                          "tier2"
                        ],
                        "description": "ONE SCAN REQUEST CAN SEND TWO OF THESE, one per tier, with different ids. `tier1` is the scan POST /scans starts (its `scanId`); when it completes, a deeper `tier2` crawl follows and sends its own. Wait for `tier1` to know the inventory is established."
                      },
                      "status": {
                        "type": "string",
                        "description": "`completed`, `partial` or `failed`."
                      },
                      "finishedAt": {
                        "type": "string",
                        "description": "ISO-8601 timestamp, UTC."
                      },
                      "pagesScanned": {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      },
                      "tagsFound": {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      },
                      "unmanagedCount": {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      },
                      "newOrChangedCount": {
                        "type": "integer",
                        "minimum": -9007199254740991,
                        "maximum": 9007199254740991
                      }
                    },
                    "required": [
                      "scanRunId",
                      "tier",
                      "status",
                      "finishedAt",
                      "pagesScanned",
                      "tagsFound",
                      "unmanagedCount",
                      "newOrChangedCount"
                    ],
                    "additionalProperties": false
                  }
                },
                "required": [
                  "id",
                  "type",
                  "version",
                  "createdAt",
                  "accountId",
                  "site",
                  "data"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is a failed attempt."
          }
        }
      }
    },
    "ping": {
      "post": {
        "operationId": "webhook_ping",
        "summary": "A test event",
        "description": "Sent when someone presses Send test on the endpoint. Never subscribed to; always signed the same way as a real event, so it proves your verification code.\n\n**Verifying a delivery.** Every POST carries `X-TagSentry-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is the HMAC-SHA256, keyed with the endpoint's secret (`whsec_…`, shown once when the endpoint is added), of the string `t + \".\" + rawBody`: the timestamp, a full stop, then the body EXACTLY as received, before any JSON parsing. Compare in constant time and refuse a `t` more than 300 seconds from your clock.\n\n```js\nconst crypto = require(\"node:crypto\");\nfunction verify(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) {\n  const parts = Object.fromEntries(header.split(\",\").map((p) => p.split(\"=\")));\n  const t = Number(parts.t);\n  if (!Number.isInteger(t) || Math.abs(nowSeconds - t) > 300) return false;\n  const expected = crypto.createHmac(\"sha256\", secret).update(`${t}.${rawBody}`).digest();\n  const given = Buffer.from(parts.v1 ?? \"\", \"hex\");\n  return given.length === expected.length && crypto.timingSafeEqual(given, expected);\n}\n```\n\n`X-TagSentry-Event` names the event; `X-TagSentry-Delivery` is the delivery id, stable across retries. Answer 2xx within 10 seconds. A timeout, 408, 429 or 5xx is retried with backoff, 8 tries in all; any other answer, and a redirect, fails at once. We never follow redirects.",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "X-TagSentry-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256 of \"t.body\">`, keyed with the endpoint's secret.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-TagSentry-Event",
            "in": "header",
            "required": true,
            "description": "The event type; equal to the body's `type`.",
            "schema": {
              "type": "string",
              "enum": [
                "ping"
              ]
            }
          },
          {
            "name": "X-TagSentry-Delivery",
            "in": "header",
            "required": true,
            "description": "The delivery id; equal to the body's `id`. Stable across retries.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "The delivery id. Stable across retries and equal to `X-TagSentry-Delivery`: dedupe on it."
                  },
                  "type": {
                    "type": "string",
                    "const": "ping"
                  },
                  "version": {
                    "type": "integer",
                    "minimum": -9007199254740991,
                    "maximum": 9007199254740991,
                    "description": "Moves only when a receiver could notice. A new optional field does not move it."
                  },
                  "createdAt": {
                    "type": "string",
                    "description": "ISO-8601 timestamp, UTC."
                  },
                  "accountId": {
                    "type": "string"
                  },
                  "site": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "string"
                      },
                      "domain": {
                        "type": "string"
                      },
                      "region": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "id",
                      "domain",
                      "region"
                    ],
                    "additionalProperties": false
                  },
                  "data": {
                    "type": "object",
                    "properties": {
                      "test": {
                        "type": "boolean",
                        "const": true,
                        "description": "Nothing happened on the site: someone pressed Send test."
                      }
                    },
                    "required": [
                      "test"
                    ],
                    "additionalProperties": false
                  }
                },
                "required": [
                  "id",
                  "type",
                  "version",
                  "createdAt",
                  "accountId",
                  "site",
                  "data"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Anything else is a failed attempt."
          }
        }
      }
    }
  }
}