{
  "openapi": "3.1.0",
  "info": {
    "title": "Hookie API",
    "version": "1.0.0",
    "summary": "Capture, route and deliver webhooks.",
    "description": "Hookie exposes three HTTP surfaces.\n\n**Ingest + streaming** is the public plane. A path-form endpoint URL authenticates by its own high-entropy slug \u2014 there is no key, token or cookie \u2014 and an `ik_live_\u2026` ingest key authenticates the `/v1/*` routes. An endpoint URL files each payload into the endpoint's own dataset through the endpoint's own criteria and mappings (none means the whole payload, as one record); `/v1/ingest/{ingest_key}/{dataset}` stores the payload whole into the named dataset. Project mapping rules are evaluated on exactly one shape, `POST /v1/ingest/{ingest_key}` with no dataset segment, and never on an endpoint URL.\n\n**The customer portal API** is a separate, token-authed surface for your end customers, scoped to one portal and one customer.\n\n**The admin API** drives the console. It accepts either a browser session or an OAuth bearer token from a connected coding agent \u2014 the same routes, the same validation, the same audit trail. An agent's authority is its user's workspace role narrowed to the scopes granted in the console. It also accepts an **admin API key** (`hk_\u2026`, Settings \u2192 API keys) for CI, infrastructure-as-code and vendor backends: the key acts as the member who created it, capped at the key's role (viewer, developer or admin \u2014 never billing or platform administration), optionally limited to one project, with an optional expiry and IP allowlist. Keys are stored as a SHA-256 hash and shown once; if their creator leaves the workspace they stop working. The same key works on the hosted MCP server at `/mcp`.\n\n**A refused agent is told how to proceed.** An agent's grant is one rung of a ladder \u2014 `hookie:read` < `hookie:write` < `hookie:manage` \u2014 and a call above it answers 403 with `code: \"agent_scope_insufficient\"`, the `required_scope`, the `granted_scopes` and a `manage_url` to the console page where its user widens it. When the user's own role is the limit the code is `insufficient_role` and there is no link, because no grant can exceed the user; billing, platform admin, managing connected agents and destination signing secrets answer `agent_not_permitted` at every scope. A human in the console still reads `Insufficient role`.\n\n**A suspended workspace** (an operator action, for abuse or non-payment) answers every surface with 403 and `reason: \"workspace_suspended\"`: ingest (endpoint URLs and ingest keys), `/v1/data` and `/v1/stream` refuse before anything is stored or counted, and every admin API write \u2014 console, OAuth agent or hosted MCP alike \u2014 is refused except billing, platform admin, connected-agent management, revoking an API key, revoking an invite, removing a member or leaving, accepting an invite to another workspace, switching workspaces and the POST-bodied searches. The customer portal API is read-only in the same way: destination writes answer 403 `workspace_suspended`. Reads keep working. Nothing is sent while it is suspended: pending deliveries are paused, workflow runs already under way are paused, open `/v1/stream` connections are closed, and cron triggers, database sources and WebSocket listeners stop. Reinstating sends the paused deliveries, resumes the paused workflow runs where they stopped, and resumes the rest; no stored data is lost.\n\n**Rate limits.** `/admin/api/*` and `/mcp` allow about 100 requests every 10 seconds per credential \u2014 per API key, per connected agent, or per signed-in person. Over that, the answer is 429 with `Retry-After` (seconds). Like every burst limit here, ingest's included, it is a best-effort guard rather than an exact count: it is counted separately at each Cloudflare location and catches up within seconds, so a short burst can get through above it. The monthly event quota is the exact count.\n\n**Idempotency.** A `POST` to `/admin/api/*` may carry an `Idempotency-Key` header (1\u2013255 printable characters, e.g. a UUID). The first request runs and its response is stored for 24 hours; a retry with the same key from the same credential, with the same path and body, gets that response back with `Idempotent-Replayed: true` and runs nothing. The same key with a different path, body or credential is refused with 422; a retry while the first request is still running gets 409. 5xx responses are not stored. `POST /admin/api/api-keys` refuses the header, because its response is a secret shown once and is never stored.\n\n**Versioning.** Every `/admin/api/*` and `/mcp` response carries `Hookie-Version` (currently `2026-09-29`). Additive changes \u2014 a new route, a new response field, a new optional parameter \u2014 ship under the current version, so clients must ignore fields they do not know. A breaking change gets a new dated version, announced in the changelog, and the previous version stays available for at least 12 months; a client pins one by sending `Hookie-Version` on its requests. A version this deployment does not serve is refused with 400 and the list of `supported_versions`.\n\n**Agent self-registration.** An AI agent can create its own account with no person involved (#331): `GET /v1/agents/register/challenge` returns a signed proof-of-work challenge, and `POST /v1/agents/register` with the solution, the agent's name, an optional operator contact and `accept_terms` set to the current Terms version creates a WorkOS identity for the agent, a workspace on the Free plan with its Default project, and an admin API key (`hk_\u2026`, role admin) returned once, with a one-time claim token a person uses later to take ownership (`POST /admin/api/agent-accounts/claim`). The key works on `/admin/api/*`, on the hosted MCP server at `/mcp` and with the CLI (`HOOKIE_TOKEN`). Until a person claims the workspace, billing, SSO and organization requests, invites, Google Workspace linking and creating more API keys are refused to it (`agent_not_permitted`), and Free-plan quotas apply in full. The registration is rate limited per IP (an IPv6 address by its /64), capped per day across the platform with no one network taking more than a tenth of the cap, asks for more proof of work as the day's cap fills, and can be switched off by the operator.\n\nThis document is generated from the request handlers and adversarially verified against them.",
    "contact": {
      "name": "Hookie",
      "url": "https://hookie.ai"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://hookie.ai/legal/terms"
    }
  },
  "servers": [
    {
      "url": "https://app.hookie.ai",
      "description": "Production"
    },
    {
      "url": "https://app.preview.hookie.ai",
      "description": "Preview"
    }
  ],
  "tags": [
    {
      "name": "Ingest",
      "description": "Send events to Hookie."
    },
    {
      "name": "Streaming",
      "description": "Tail events in real time over SSE or WebSocket."
    },
    {
      "name": "Portal",
      "description": "Token-authed surface for your end customers."
    },
    {
      "name": "Projects",
      "description": "Projects and their settings."
    },
    {
      "name": "Routing",
      "description": "Rules, endpoints, datasets and records."
    },
    {
      "name": "Delivery",
      "description": "Destinations, deliveries and replay."
    },
    {
      "name": "Workflows",
      "description": "Multi-step workflows, triggers and AI agents."
    },
    {
      "name": "Observability",
      "description": "Search, correlation, stats and the audit log."
    },
    {
      "name": "Workspace",
      "description": "The workspace itself: its name and slug, its members and their roles, shareable-link invites, ownership transfer, the switcher between the workspaces a person belongs to, admin API keys, data export and closure."
    },
    {
      "name": "Data API",
      "description": "Read-only access to the datasets and columns a project exposes, with per-project keys."
    },
    {
      "name": "Broadcast Logs",
      "description": "Forward a project's logs and traces over OTLP/HTTP (JSON) to Datadog, Grafana Cloud, an OpenTelemetry Collector, PostHog or Sentry."
    },
    {
      "name": "Status",
      "description": "Whether the API can answer at all: the public health check that hookie.ai/status reads."
    },
    {
      "name": "Agents",
      "description": "Agent self-registration (#331): an AI agent creates its own Hookie account, with no person involved, and gets an admin API key for the admin API, the hosted MCP server and the CLI. The account starts on the Free plan; a person can later claim it with the one-time claim token. See docs.hookie.ai \u2192 Agents and the design note docs/design/agent-self-registration.md."
    }
  ],
  "components": {
    "securitySchemes": {
      "ingestKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An `ik_live_\u2026` ingest key. Stored hashed; the plaintext is returned once at creation."
      },
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "hookie_session",
        "description": "The console's sealed session cookie, set by WorkOS AuthKit. Mutating requests also require the `X-Requested-With` CSRF header."
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "An OAuth 2.1 access token from a connected agent, audienced at the `/mcp` resource URI. The agent acts as its user, with that user's role narrowed to the granted `hookie:*` scopes. No CSRF header is required \u2014 a bearer token is not ambient credentials."
      },
      "portalTokenHeader": {
        "type": "http",
        "scheme": "bearer",
        "description": "A `hpt_\u2026` customer portal token, as `Authorization: Bearer`. Stored hashed and looked up by its full SHA-256 hash; shown once at issue. The query string (`?token=`) is not accepted: a token there ends up in request logs and traces. A browser page trades the token for a `portalSession` cookie with POST /portal/api/session."
      },
      "dataApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A project Data API key (hdk_\u2026), created in the project's Settings \u2192 Data API."
      },
      "portalSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-hookie_portal",
        "description": "The customer portal's session cookie, set by POST /portal/api/session: sealed (AES-GCM), HttpOnly, Secure, SameSite=None, Partitioned, and valid for 8 hours or until the token expires, whichever is sooner. It names the token rather than copying what it grants, so revoking the token or switching the portal off ends the session at once. Requests other than GET made with it must carry `X-Requested-With: fetch` (CSRF)."
      },
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An `hk_\u2026` admin API key from Settings \u2192 API keys. Acts as the member who created it, capped at the key's role (viewer, developer or admin) and optionally one project. Stored as a SHA-256 hash, shown once. No CSRF header is required."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable reason."
          },
          "retry_after": {
            "type": "integer",
            "description": "Seconds to wait. Present on 429."
          },
          "upgrade_url": {
            "type": "string",
            "format": "uri",
            "description": "Present on a monthly-quota 429, and on a `member_limit_reached` 403 (the console's Settings \u2192 Plan & billing)."
          },
          "reason": {
            "type": "string",
            "description": "Machine-readable reason, where the status alone is ambiguous. `workspace_suspended` (403): the workspace is suspended; `delivery_allowance_exhausted` (429): a replay was refused because the monthly delivery allowance is used. `endpoint_disabled` (503): the endpoint is switched off; `handshake_failed`: a provider URL-verification challenge could not be answered (#193). `missing_header`, `bad_signature`, `stale_timestamp` (401): an endpoint's provider signature check failed; `scheme` names the check. `portal_customer_mismatch` (409): a portal token was requested for, or the vendor switched on the endpoint of, a customer other than the one the portal now serves (PORT-2g); `customer_id` and `customer_name` name that customer. Agent self-registration (#331), on /v1/agents/register: `rate_limited` (429), `agent_registration_disabled` and `agent_registration_unavailable` (503), `invalid_body`, `invalid_agent_name`, `invalid_operator_contact`, `terms_not_accepted` (with `terms_version`), `proof_of_work_required`, `invalid_challenge`, `challenge_expired`, `invalid_proof_of_work`, `invalid_idempotency_key` (400), `body_too_large` (413), `already_registered`, `registration_in_progress`, `challenge_spent` (409, with `account_id`), `daily_cap_reached`, `source_daily_limit` (429), `identity_provider_error` (502), `registration_incomplete` (503)."
          },
          "code": {
            "type": "string",
            "enum": [
              "agent_scope_insufficient",
              "insufficient_role",
              "agent_not_permitted",
              "member_limit_reached"
            ],
            "description": "Present on a 403 to an OAuth-connected agent, and on a member-limit 403 to anyone. `agent_scope_insufficient`: the agent's grant is below what the call needs, and widening it would let the call through (see `required_scope` and `manage_url`). `insufficient_role`: the user's OWN workspace role does not allow the call, so no grant can \u2014 a workspace owner or admin has to change the user's role first, in the console under Settings \u2192 Members. `agent_not_permitted`: no connected agent or API key may do this at any scope (billing, platform admin, managing connected agents, a destination's signing secret, invites of any kind, changing a role, removing a member, transferring ownership, listing or switching a person's workspaces, and creating API keys). To a self-registered agent account nobody has claimed yet (#331) it also carries `claim_required: true`: a person must claim the workspace with the claim link from registration before anyone can do it. `member_limit_reached` (#308): the workspace already has as many members as its plan allows, so an invite cannot be created or accepted; `max_members`, `members` and `upgrade_url` say how many and where to upgrade."
          },
          "required_scope": {
            "type": "string",
            "enum": [
              "hookie:read",
              "hookie:write",
              "hookie:manage"
            ],
            "description": "With `code`: the scope the refused call needs."
          },
          "granted_scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "With `code`: the scopes the agent's grant holds now."
          },
          "manage_url": {
            "type": "string",
            "format": "uri",
            "description": "With `code: \"agent_scope_insufficient\"` only: the console's Settings \u2192 Connected agents page, opened on this agent (`\u2026/#/settings/agents?client=<client_id>`), where its user widens the grant. The change applies on the agent's next request."
          },
          "scheme": {
            "type": "string",
            "description": "Present on a signature 401: the endpoint's verification scheme."
          },
          "max_members": {
            "type": "integer",
            "description": "With `code: \"member_limit_reached\"`: the plan's member limit."
          },
          "members": {
            "type": "integer",
            "description": "With `code: \"member_limit_reached\"`: the workspace's members now."
          },
          "customer_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "On a `portal_customer_mismatch` 409: the customer the portal already serves."
          },
          "customer_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "On a `portal_customer_mismatch` 409: that customer's display name, if one was given."
          },
          "account_id": {
            "type": "string",
            "description": "On a 409 from POST /v1/agents/register: the agent account the earlier request created (or is creating). Never accompanied by a credential."
          },
          "terms_version": {
            "type": "string",
            "description": "On a `terms_not_accepted` 400: the Terms version `accept_terms` must equal."
          },
          "claim_required": {
            "type": "boolean",
            "description": "With `code: \"agent_not_permitted\"`, to a self-registered agent account nobody has claimed (#331): the refused action needs a person, who first claims the workspace with the claim link from the registration response (`claim.url`)."
          }
        }
      },
      "DeliveryEnvelope": {
        "type": "object",
        "description": "The body of every outbound delivery. Always this envelope; the record's payload is under data. Verify signatures over the raw bytes as received, never a re-serialisation.",
        "required": [
          "id",
          "dataset",
          "received_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The record (event) id. The same on every retry and replay of it."
          },
          "dataset": {
            "type": "string"
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "description": "The record's payload, as routed."
          }
        }
      },
      "EndpointVerificationInput": {
        "type": "object",
        "required": [
          "scheme"
        ],
        "additionalProperties": false,
        "description": "Provider signature verification (#210). Ingest checks the signature over the raw request bytes before idempotency, the quota and the store; a missing header, a forged signature or a stale timestamp answers 401 {reason, scheme} and is neither stored nor counted, and writes an ingest_signature_rejected audit row. Applies on the public path-form URL and on /v1/webhooks/{ingest_key}/{slug}. Settings that do not apply to the scheme are refused with 400.",
        "properties": {
          "scheme": {
            "type": "string",
            "enum": [
              "none",
              "stripe",
              "github",
              "shopify",
              "slack",
              "standard_webhooks",
              "hmac"
            ],
            "description": "none (default): no check. stripe: Stripe-Signature t=,v1= over `<t>.<body>`. github: X-Hub-Signature-256 sha256=<hex>. shopify: X-Shopify-Hmac-Sha256, base64. slack: X-Slack-Signature v0=<hex> over `v0:<X-Slack-Request-Timestamp>:<body>`. standard_webhooks: webhook-id / webhook-timestamp / webhook-signature (svix-* accepted too), secret whsec_<base64>. hmac: generic HMAC of the body in a configurable header."
          },
          "secret": {
            "type": "string",
            "minLength": 1,
            "maxLength": 512,
            "writeOnly": true,
            "description": "The provider's signing secret. Stored AES-256-GCM encrypted and never returned by any response, audit row or log. Required when turning a scheme on, unless a create sends without_secret: true; on PATCH it may be omitted to keep what is held (a secret, or none for an endpoint created without one), under the same scheme only. A secret set here replaces the old one at once; use rotate-verification-secret for an overlap."
          },
          "without_secret": {
            "type": "boolean",
            "const": true,
            "description": "Create only (#238): make the endpoint verify this scheme with NO secret yet. It then refuses every request with 401 (reason bad_signature, \"This endpoint requires a signature but has no secret to check it with\") until a secret is set with PATCH, and responses say has_secret: false. This is how a configuration import, a clone and `hookie apply` recreate an endpoint's verification, since a secret is never exported. Refused with secret, with scheme none, and on PATCH."
          },
          "header": {
            "type": "string",
            "default": "X-Signature",
            "description": "hmac only: the header carrying the signature."
          },
          "algorithm": {
            "type": "string",
            "enum": [
              "sha256",
              "sha1"
            ],
            "default": "sha256",
            "description": "hmac only."
          },
          "encoding": {
            "type": "string",
            "enum": [
              "hex",
              "base64"
            ],
            "default": "hex",
            "description": "hmac only."
          },
          "prefix": {
            "type": "string",
            "maxLength": 32,
            "description": "hmac only: text before the digest, e.g. sha256=."
          },
          "tolerance_seconds": {
            "type": "integer",
            "minimum": 30,
            "maximum": 86400,
            "default": 300,
            "description": "stripe, slack and standard_webhooks only: how far the signed timestamp may sit from now."
          }
        },
        "example": {
          "scheme": "stripe",
          "secret": "whsec_..."
        }
      },
      "EndpointVerification": {
        "type": "object",
        "required": [
          "scheme"
        ],
        "description": "An endpoint's signature verification as responses describe it: the scheme and its settings, never the secret.",
        "properties": {
          "scheme": {
            "type": "string",
            "enum": [
              "none",
              "stripe",
              "github",
              "shopify",
              "slack",
              "standard_webhooks",
              "hmac"
            ]
          },
          "header": {
            "type": "string"
          },
          "algorithm": {
            "type": "string",
            "enum": [
              "sha256",
              "sha1"
            ]
          },
          "encoding": {
            "type": "string",
            "enum": [
              "hex",
              "base64"
            ]
          },
          "prefix": {
            "type": "string"
          },
          "tolerance_seconds": {
            "type": "integer"
          },
          "has_secret": {
            "type": "boolean",
            "description": "Whether a secret is held. Absent when scheme is none. false for an endpoint created with without_secret (for example by a configuration import or clone): it refuses every request with 401 until a secret is set."
          },
          "previous_secret_expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "After a rotation, when the replaced secret stops verifying; null outside an overlap. Absent when scheme is none."
          }
        }
      },
      "Condition": {
        "type": "object",
        "description": "One condition of the one condition dialect (#165, extended in #201), accepted by rules, endpoint criteria, workflow entry/branch/wait conditions, AI-trigger match conditions and WebSocket-trigger conditions. Either {path, op, value} or the shorthand {path, equals}. op: equals | not_equals | contains | exists | gt | gte | lt | lte | in | regex. exists takes no value; gt/gte/lt/lte need a finite number and match a numeric field, or a string field that is a plain decimal once trimmed (optional sign, digits, optional fraction), such as a form's \"150\" (#259); hex, exponents, empty and other text never match; contains needs a string; in needs a list of 1-100 scalars; regex needs a pattern of at most 200 characters that compiles, with no backreferences, no repeated group that itself repeats or alternates ((a+)+, (a|b)*) and at most 2 open-ended repetitions (*, +, {n,}), and is tested against the first 256 characters of a string field. In rules and endpoint criteria equals, not_equals and in compare as TEXT (an exact match is stored as {path, equals}); in workflows and triggers they compare with ===. All conditions in a list must hold (AND); an empty list matches everything. An invalid condition is refused with 400 when saved.",
        "required": [
          "path"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "Dot path into the payload."
          },
          "op": {
            "type": "string",
            "enum": [
              "equals",
              "not_equals",
              "contains",
              "exists",
              "gt",
              "gte",
              "lt",
              "lte",
              "in",
              "regex"
            ]
          },
          "value": {
            "description": "Required for every op but exists."
          },
          "equals": {
            "description": "Shorthand for op equals with this value; not together with op/value."
          }
        },
        "additionalProperties": false
      },
      "ProjectConfig": {
        "type": "object",
        "description": "A project's configuration (DX-10, #201) \u2014 the hookie.yml shape, so `hookie apply` reads an export as it is. NOTHING SECRET IS IN IT: no endpoint URL (its slug is the credential), endpoint verification secret, destination signing secret or ingest key. Sources, WebSocket triggers and portals hold encrypted credentials and are listed under not_exported instead. Identity per kind: an endpoint's slug, a record view's dataset + name, everything else's name.",
        "properties": {
          "version": {
            "type": "integer",
            "const": 1
          },
          "project": {
            "type": [
              "string",
              "null"
            ],
            "description": "The source project's slug; informational."
          },
          "endpoints": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "{name, slug, dataset, enabled, criteria, mappings, ip_allowlist?, verification} \u2014 one per endpoint, its newest version. verification (#238) is the endpoint's signature scheme and non-secret settings, {scheme, header?, algorithm?, encoding?, prefix?, tolerance_seconds?} ({scheme: \"none\"} when it does not verify); never the secret, the previous secret or has_secret. An import recreates a verifying scheme with no secret (without_secret: true), so the copy refuses every request with 401 until a secret is set, and lists it under needs_verification_secret. An entry without verification is created without it."
            }
          },
          "rules": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "{name, dataset, enabled, conditions, mappings}"
            }
          },
          "destinations": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "{name, url, enabled, dataset_filter?, timeout_seconds?, max_per_second?, max_concurrency?} \u2014 portal-owned destinations excluded."
            }
          },
          "ai_agents": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "{ref, name, active, model, system_prompt, tools, memory_dataset?, max_tokens?}. ref is the source agent's id: an import points a workflow agent_call that names it at the agent it creates."
            }
          },
          "workflows": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "{name, slug, active, entry_dataset, entry_conditions, steps}"
            }
          },
          "triggers": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "AI triggers: {name, enabled, match, action_type, config}"
            }
          },
          "cron_triggers": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "{name, enabled, schedule, timezone, payload, dataset}"
            }
          },
          "record_views": {
            "type": "array",
            "items": {
              "type": "object",
              "description": "{dataset, name, columns, sort_key, sort_dir, is_default}"
            }
          },
          "not_exported": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "reason": {
                  "type": "string"
                }
              }
            },
            "description": "Informational; an import ignores it."
          }
        }
      },
      "ConfigImportResult": {
        "type": "object",
        "required": [
          "ok",
          "created",
          "skipped",
          "failed",
          "needs_verification_secret"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "True when nothing failed."
          },
          "created": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "id": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "note": {
                  "type": "string",
                  "description": "Set when an endpoint's slug was taken in the workspace (slugs are unique per workspace) and it was created as slug-copy, and when an endpoint was created verifying a scheme with no secret (it refuses every request until one is set)."
                }
              }
            }
          },
          "skipped": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                }
              }
            }
          },
          "failed": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "kind": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "error": {
                  "type": "string"
                }
              }
            }
          },
          "needs_verification_secret": {
            "type": "array",
            "description": "Endpoints created verifying a provider's signature but holding NO secret (#238): the document carried the scheme and settings, never a secret. Each refuses every request with 401 until its secret is set (console Edit \u2192 Verification, or PATCH webhooks/{id} with verification {scheme, secret}). slug is the endpoint's slug as created (with any -copy suffix).",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "slug",
                "scheme"
              ],
              "properties": {
                "id": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "name": {
                  "type": "string"
                },
                "slug": {
                  "type": "string"
                },
                "scheme": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "EraseCounts": {
        "type": "object",
        "description": "What an erasure removed (SEC-6, #195).",
        "required": [
          "records",
          "submissions",
          "deliveries",
          "workflow_runs"
        ],
        "properties": {
          "records": {
            "type": "integer"
          },
          "submissions": {
            "type": "integer",
            "description": "Raw submissions deleted. A submission that routed into several records goes with the first of them deleted; the others keep their own payload with submission_id cleared."
          },
          "deliveries": {
            "type": "integer",
            "description": "Delivery rows deleted, replays (`<record>#replay:<n>`) included, with their stored response bodies."
          },
          "workflow_runs": {
            "type": "integer",
            "description": "Workflow instances the record started (with their step log, waiters and AI calls)."
          }
        }
      },
      "DatasetPurge": {
        "type": "object",
        "required": [
          "id",
          "dataset",
          "status",
          "records_deleted",
          "requested_at",
          "finished_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "dataset": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "running",
              "done"
            ]
          },
          "records_deleted": {
            "type": "integer"
          },
          "records_remaining": {
            "type": "integer",
            "description": "Records still to delete (received at or before the request). 0 once done."
          },
          "requested_at": {
            "type": "string",
            "format": "date-time",
            "description": "Also the cutoff: records received after it are not part of the purge."
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "WorkspaceClosure": {
        "type": "object",
        "required": [
          "requested_at",
          "purge_at",
          "grace_days"
        ],
        "properties": {
          "requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "purge_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the scheduled sweep starts deleting the workspace's data. Until then the owner can undo the closure."
          },
          "grace_days": {
            "type": "integer",
            "description": "Days between closing and deletion (14)."
          }
        }
      },
      "RetentionGrace": {
        "type": "object",
        "required": [
          "retention_days",
          "plan_retention_days",
          "ends_at"
        ],
        "properties": {
          "retention_days": {
            "type": "integer",
            "description": "The retention, in days, the retention prune keeps until `ends_at`: the plan's before the downgrade, or the longest of them after repeat downgrades."
          },
          "plan_retention_days": {
            "type": "integer",
            "description": "The current plan's retention, in days: what applies from `ends_at` on."
          },
          "ends_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the window closes. The first retention prune after it (every 5 minutes) permanently deletes event data older than `plan_retention_days`."
          }
        },
        "description": "A retention grace window (#281). For 7 days after a plan change that shortens retention (a Stripe downgrade or cancellation, a lapsed trial, an operator override), the retention prune keeps the retention that was in force before it. A second downgrade inside the window keeps the longest retention and restarts the 7 days; an upgrade to a plan whose retention covers `retention_days` ends it."
      },
      "ProjectError": {
        "type": "object",
        "required": [
          "id",
          "received_at",
          "kind",
          "forward_status",
          "endpoint_id",
          "endpoint_name",
          "source",
          "reason",
          "payload",
          "payload_length",
          "payload_truncated",
          "content_type"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The submission id. Correlate it in Observability: POST \u2026/search/correlate/{id}."
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          },
          "kind": {
            "type": "string",
            "enum": [
              "filtered",
              "routing_failed"
            ],
            "description": "filtered: the endpoint's own criteria rejected the payload. routing_failed: the submission was stored but its records could not be written (forward_status 'failed'); the outbox sweep keeps retrying it, and it leaves this list once it is routed."
          },
          "forward_status": {
            "type": "string",
            "enum": [
              "filtered",
              "failed"
            ]
          },
          "endpoint_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The endpoint it arrived on; null for an event from an ingest key or a trigger."
          },
          "endpoint_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "That endpoint's name; null when there is none or it has been deleted."
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where it came from: webhook:<endpoint id or slug>, ingest, cron:<id>, ws:<id>, source:<id>."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why, in a short sentence: for filtered, the first criterion the payload did not meet (e.g. \"Did not match the endpoint's criteria: type equals \\\"order.created\\\"\"); for routing_failed, the latest routing error. Null on a row from before #287."
          },
          "payload": {
            "type": "string",
            "description": "The stored body. In the list, its first 500 characters; from GET \u2026/errors/{id}, whole."
          },
          "payload_length": {
            "type": "integer",
            "description": "The stored body's full length in characters."
          },
          "payload_truncated": {
            "type": "boolean"
          },
          "content_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "The request's media type (ING-3); null for a trigger's event."
          }
        }
      },
      "Member": {
        "type": "object",
        "required": [
          "user_id",
          "email",
          "name",
          "role",
          "joined_at"
        ],
        "properties": {
          "user_id": {
            "type": "string",
            "description": "The member's user id; the path parameter of PATCH and DELETE /admin/api/members/{user_id}."
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "developer",
              "viewer",
              "member"
            ],
            "description": "`member` is the legacy read-only role, treated as viewer."
          },
          "joined_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Invite": {
        "type": "object",
        "required": [
          "id",
          "role",
          "created_by",
          "created_by_email",
          "created_at",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "developer",
              "viewer"
            ],
            "description": "The role whoever accepts the link joins with. Never owner."
          },
          "created_by": {
            "type": "string",
            "description": "User id of the member who created the link."
          },
          "created_by_email": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Seven days after created_at."
          }
        }
      },
      "WorkspaceMembership": {
        "type": "object",
        "required": [
          "tenant_id",
          "name",
          "slug",
          "role",
          "plan",
          "status",
          "active"
        ],
        "properties": {
          "tenant_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "developer",
              "viewer",
              "member"
            ],
            "description": "The caller's role in that workspace."
          },
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "standard",
              "team"
            ],
            "description": "`standard` is Pro."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "suspended"
            ]
          },
          "active": {
            "type": "boolean",
            "description": "True for the workspace this request was answered in."
          }
        }
      },
      "DestinationType": {
        "type": "string",
        "enum": [
          "webhook",
          "slack",
          "s3",
          "sqs",
          "pubsub"
        ],
        "default": "webhook",
        "description": "How a delivery leaves Hookie (DLV-13, #313, #322). `webhook`: the signed HTTPS POST to `url` (Hookie-Signature and the Standard Webhooks headers), unchanged. `slack`: a POST to a Slack incoming webhook with a readable message ({text, blocks}: dataset, event id, received time and the payload as pretty JSON truncated to 2,500 characters), or the destination's own template when it has a transform; it carries only Content-Type and User-Agent, no signature headers. `s3`: one object per event PUT to an S3-compatible bucket (AWS S3, Cloudflare R2, GCS interoperability, MinIO) at {prefix}{dataset}/{yyyy}/{mm}/{dd}/{event id}.json (dated by when the event was received, UTC), Content-Type application/json, x-amz-meta-hookie-event-id and x-amz-meta-hookie-dataset, signed with AWS SigV4; the key is the event's, so a retry or replay overwrites the same object. `sqs` (#322): one SendMessage per event to an Amazon SQS queue over the JSON protocol (POST to the queue URL's host, X-Amz-Target AmazonSQS.SendMessage, Content-Type application/x-amz-json-1.0, signed with AWS SigV4 for service sqs); MessageBody is the delivery body, and the message attributes event_id and dataset (String) carry the event's id and dataset. A FIFO queue (name ending .fifo) also gets MessageDeduplicationId = the event id and MessageGroupId = the dataset, so a retry or replay inside SQS's five-minute window is one message. A message over SQS's 1 MiB limit (1,048,576 bytes, body and attributes together) fails at once and is not retried. SQS's error code decides a retry: throttling (ThrottlingException, RequestThrottled, KmsThrottled) and SQS-side failures are retried, refusals (AccessDenied, QueueDoesNotExist, InvalidParameterValue, InvalidMessageContents, signature and KMS errors) are not; the code is added to the delivery's error. `pubsub` (#322): one publish per event, POST https://pubsub.googleapis.com/v1/projects/{project}/topics/{topic}:publish with {messages: [{data: base64(body), attributes: {event_id, dataset}}]}, authorised by an OAuth access token minted from the destination's service-account key (an RS256 JWT exchanged at https://oauth2.googleapis.com/token) and cached until 60 s before it expires, never minted per delivery. A 401 drops the cached token and is retried; a key Google refuses fails the delivery without retrying. Pub/Sub has no deduplication on publish: receivers dedupe on the event_id attribute. All of them share retries with backoff, pacing, the delivery allowance and holds, replay and the delivery log. The type is fixed at creation."
      },
      "DestinationConfigInput": {
        "description": "A typed destination's settings (DLV-13, #313, #322). Required on create for those types; refused for a webhook. Credentials are stored AES-256-GCM encrypted and never returned, exported or logged. On PATCH, a credential that is not sent keeps the stored one, and settings that are not sent keep their stored values. On create, the Slack variant needs webhook_url.",
        "anyOf": [
          {
            "type": "object",
            "title": "Slack",
            "additionalProperties": false,
            "properties": {
              "webhook_url": {
                "type": "string",
                "format": "uri",
                "maxLength": 500,
                "description": "A Slack incoming-webhook URL: https://hooks.slack.com/services/T\u2026/B\u2026/\u2026 (also /workflows/ and /triggers/). Any other host is refused. The URL is the credential: it is shown back only masked (https://hooks.slack.com/services/\u2022\u2022\u2022\u2022abcd). Sending that masked value back unchanged keeps the stored URL."
              }
            }
          },
          {
            "type": "object",
            "title": "S3",
            "additionalProperties": false,
            "properties": {
              "endpoint": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "description": "Origin of an S3-compatible service, https and a public host only, with no path: https://<account>.r2.cloudflarestorage.com for R2, https://storage.googleapis.com for GCS. Omitted or null: AWS S3 in `region`. Objects on a custom endpoint are addressed path-style (https://host/bucket/key)."
              },
              "region": {
                "type": "string",
                "description": "The signing region: us-east-1, eu-west-2, \u2026; `auto` for R2. Required for AWS; defaults to `auto` when an endpoint is given."
              },
              "bucket": {
                "type": "string",
                "pattern": "^[a-z0-9][a-z0-9._-]{1,61}[a-z0-9]$"
              },
              "prefix": {
                "type": "string",
                "maxLength": 256,
                "default": "",
                "description": "Prepended to every key as is (end it with / for a folder). May not start with /, or contain . or .. segments, backslashes or control characters."
              },
              "access_key_id": {
                "type": "string",
                "description": "Returned in full: it identifies the key, it does not unlock it."
              },
              "secret_access_key": {
                "type": "string",
                "maxLength": 256,
                "description": "Write-only. Never returned."
              }
            },
            "description": "On create, bucket, access_key_id and secret_access_key are required, and region unless endpoint is set. On PATCH, send only what changes."
          },
          {
            "type": "object",
            "title": "SQS",
            "additionalProperties": false,
            "properties": {
              "queue_url": {
                "type": "string",
                "format": "uri",
                "maxLength": 300,
                "description": "The queue's URL: https://sqs.<region>.amazonaws.com/<12-digit account id>/<queue name> (also sqs.<region>.amazonaws.com.cn, sqs-fips.<region>.amazonaws.com and the legacy <region>.queue.amazonaws.com and queue.amazonaws.com). Any other host is refused. A name ending .fifo is a FIFO queue."
              },
              "region": {
                "type": "string",
                "description": "Optional: read from the queue URL. Sent, it must match it."
              },
              "access_key_id": {
                "type": "string",
                "description": "Returned in full: it identifies the key, it does not unlock it. Grant it sqs:SendMessage on this queue and nothing more."
              },
              "secret_access_key": {
                "type": "string",
                "maxLength": 256,
                "description": "Write-only. Never returned."
              }
            },
            "description": "On create, queue_url, access_key_id and secret_access_key are required. On PATCH, send only what changes."
          },
          {
            "type": "object",
            "title": "Pub/Sub",
            "additionalProperties": false,
            "properties": {
              "project_id": {
                "type": "string",
                "pattern": "^[a-z][a-z0-9-]{4,28}[a-z0-9]$",
                "description": "The Google Cloud project the topic is in."
              },
              "topic": {
                "type": "string",
                "description": "The topic id (3-255 letters, digits, - _ . ~ +, starting with a letter, not goog...), or its full name projects/{project}/topics/{topic}."
              },
              "service_account_json": {
                "type": [
                  "string",
                  "object"
                ],
                "maxLength": 16384,
                "description": "Write-only. The service account's JSON key file as Google issues it (text or object): it must have type service_account, a client_email of a service account, an RSA private_key in PKCS #8 PEM, and token_uri https://oauth2.googleapis.com/token (any other token_uri is refused). Only the email, the private key and its id are kept, encrypted; the email is returned as config.service_account_email. Scope the account to roles/pubsub.publisher on this topic."
              }
            },
            "description": "On create, project_id, topic and service_account_json are required. On PATCH, send only what changes; a new service_account_json drops the cached access token."
          }
        ]
      },
      "DestinationConfig": {
        "type": [
          "object",
          "null"
        ],
        "description": "The type's settings as returned (DLV-13, #322): null for a webhook; {webhook_url} (masked) for Slack; {endpoint, region, bucket, prefix, access_key_id} for S3; {queue_url, region, access_key_id, fifo} for SQS; {project_id, topic, service_account_email} for Pub/Sub. No credential is ever included.",
        "properties": {
          "webhook_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Slack: the masked URL, e.g. https://hooks.slack.com/services/\u2022\u2022\u2022\u2022abcd."
          },
          "endpoint": {
            "type": [
              "string",
              "null"
            ]
          },
          "region": {
            "type": [
              "string",
              "null"
            ]
          },
          "bucket": {
            "type": [
              "string",
              "null"
            ]
          },
          "prefix": {
            "type": "string"
          },
          "access_key_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "queue_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "SQS: the queue's URL."
          },
          "fifo": {
            "type": "boolean",
            "description": "SQS: whether the queue is FIFO (its name ends .fifo), so messages carry MessageDeduplicationId and MessageGroupId."
          },
          "project_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pub/Sub: the topic's project."
          },
          "topic": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pub/Sub: the topic id."
          },
          "service_account_email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pub/Sub: the service account whose key is stored (the key itself is never returned)."
          }
        }
      },
      "GoogleWorkspaceLink": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "domain",
              "default_role",
              "created_by",
              "created_by_email",
              "created_at"
            ],
            "properties": {
              "domain": {
                "type": "string",
                "description": "The Google Workspace domain (Google's `hd`), lower case."
              },
              "default_role": {
                "type": "string",
                "enum": [
                  "viewer",
                  "developer"
                ],
                "description": "The role a person joins with."
              },
              "created_by": {
                "type": "string",
                "description": "The owner who linked it."
              },
              "created_by_email": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "created_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          {
            "type": "null"
          }
        ]
      },
      "SsoOrgRequest": {
        "type": "object",
        "description": "A workspace's request for a WorkOS organization, for single sign-on and/or Directory Sync (#325). Filing one creates nothing in WorkOS: a Hookie operator approves or rejects it, and only approval creates the organization.",
        "required": [
          "id",
          "kinds",
          "domains",
          "note",
          "status",
          "requested_at",
          "decided_at",
          "reason"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "sreq_6f1c0a7e2b8d4c1e9a3f5b7d0c2e4a6f"
          },
          "kinds": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "sso",
                "dsync"
              ]
            },
            "description": "What the workspace asked for: `sso` (single sign-on) and/or `dsync` (Directory Sync). Each is an Admin Portal intent the owner may open once approved."
          },
          "domains": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "example": [
              "acme.com"
            ],
            "description": "Lower-cased email domains. WorkOS receives them as pending; the customer proves each one by DNS in the Admin Portal."
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approving",
              "approved",
              "rejected",
              "cancelled",
              "superseded"
            ],
            "description": "`approving` is the moment an operator's decision is with WorkOS; it ends in `approved`, or back in `pending` if WorkOS refused. `superseded`: an approved request replaced by an approved change."
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "decided_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "The operator's reason, when rejected."
          },
          "supersedes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set on a change request: the approved request it would replace. Approving the change updates the same WorkOS organization (its domains are set to the change's) instead of creating a second one."
          }
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Makes a POST safe to retry for 24 hours: the same key, credential, path and body returns the stored response (`Idempotent-Replayed: true`) instead of running again. 422 if the key was used for a different request; 409 while the first is in flight.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Page size, 1-1000. Omitted, the whole list up to 1000 rows (#201).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 1000,
          "default": 1000
        }
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "required": false,
        "description": "Rows to skip (#201).",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Over this credential's burst limit (about 100 requests per 10 seconds; best-effort, counted separately at each Cloudflare location). Retry after `Retry-After` seconds.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/{workspace}/{project}/{webhook}": {
      "post": {
        "operationId": "ingestPathForm",
        "summary": "Ingest an event through the public webhook URL",
        "tags": [
          "Ingest"
        ],
        "responses": {
          "200": {
            "description": "Duplicate - this endpoint (or, on /v1/ingest, this ingest key) already stored a submission with the same dedup key, so the retry is answered with the first submission's id and nothing is stored or counted (ING-9, #193). The dedup key is, in order: an Idempotency-Key header; else a provider delivery id kept the same across that provider's retries - webhook-id (Standard Webhooks), svix-id, X-GitHub-Delivery, X-Shopify-Webhook-Id, X-Gitlab-Event-UUID, I-Twilio-Idempotency-Token, Linear-Delivery, Twitch-Eventsub-Message-Id, X-Atlassian-Webhook-Identifier; else Stripe's event id (a body with object 'event' and an evt_ id) or Slack's event_id (type 'event_callback'). The key space is the ENDPOINT's, not the workspace's: two endpoints may receive the same key. `deduplicated_by` names which source matched. A request that loses the unique-index race has already been counted against the monthly quota. ALSO 200 for an answered URL-verification challenge (ING-10): the challenge as text/plain, or Zoom's JSON {plainToken, encryptedToken}. Nothing is stored or counted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "idempotent"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string",
                      "description": "id of the submission stored by the first request"
                    },
                    "idempotent": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "deduplicated_by": {
                      "type": "string",
                      "enum": [
                        "idempotency-key",
                        "standard-webhooks",
                        "svix",
                        "github",
                        "shopify",
                        "gitlab",
                        "twilio",
                        "linear",
                        "twitch",
                        "atlassian",
                        "stripe",
                        "slack"
                      ],
                      "description": "Which dedup source matched."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Stored. The endpoint's own criteria and mappings apply (ING-1); project rules do not run on this URL. If the criteria are empty or match, the payload (mapped, or whole when the endpoint has no mappings) becomes one record in the endpoint's dataset and 'routed' holds that dataset name. If the criteria do not match, the submission is still stored and counted and the response is 201 with routed: [] and records: 0. Routing errors after the submission row is written are swallowed (the raw submission is never lost, it is marked 'failed' and the outbox sweep routes it again), which looks the same except that filtered is absent. A payload the criteria reject is answered with filtered: true as well (#287), and its submission is marked forward_status 'filtered', with the criterion it did not meet as the reason; a matching payload's response is unchanged and carries no filtered field.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "routed",
                    "records"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "routed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "the dataset the record landed in"
                    },
                    "records": {
                      "type": "integer",
                      "description": "routed.length"
                    },
                    "filtered": {
                      "type": "boolean",
                      "enum": [
                        true
                      ],
                      "description": "Present, and true, only when the endpoint's own criteria rejected the payload (#287). The submission is kept (and counted against the quota) with forward_status 'filtered' and no record, and is listed on the project's Errors tab: GET /admin/api/projects/{project_id}/errors."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body could not be parsed: 'Body must be valid JSON' (the body declares application/json, a +json type or no Content-Type, and does not parse) or 'Body is not valid multipart/form-data'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota. Also reason 'handshake_failed' when the endpoint's URL-verification challenge arrives without a usable challenge or token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "error: 'Source IP not allowed'. The Cloudflare client IP must match BOTH the webhook's IP allowlist and the tenant's (an empty allowlist matches everything; a request with no client IP skips the check). The block is written to the audit log. Also `reason: \"workspace_suspended\"` when the workspace has been suspended by the operator: nothing is stored, counted or charged, so a sender should retry after the workspace is reinstated. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "error: 'Unknown endpoint'. Uniform miss for any of: unknown workspace slug, unknown project slug, unknown webhook slug. A webhook that exists but is disabled answers 503 instead. After a rotation the previous webhook slug is still accepted until its overlap ends, then it is 404 like any unknown slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "error: 'Body exceeds 1000000 bytes'. Checked against Content-Length first, then against the buffered byte length. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The body is not JSON and its Content-Type is neither a form encoding nor a textual type that can be stored as {body, content_type} (for example application/octet-stream or image/png). The error names the Content-Type. Nothing is stored and no quota is charged. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Two cases, each with a Retry-After response header in seconds that equals the retry_after body field. Edge burst limit: about 100 requests per 10 seconds per credential (one endpoint URL, or one ingest key), the same on every plan: {error: 'Rate limit exceeded', retry_after: 10} with Retry-After: 10. It is a best-effort burst guard, not an exact count: it is counted separately at each Cloudflare location and catches up within seconds, so a short burst can get through above it. Monthly plan quota, the exact count (the plan's events plus its soft cap: +10% on Pro and Team, none on Free): {error: 'Monthly event quota reached: this event was not stored. The quota resets at <reset_at>; upgrade the plan to raise it sooner.', reason: 'quota_exceeded', limit: <events at which ingest refuses>, reset_at: '<00:00 UTC on the 1st of next month>', upgrade_url: '<APP_ORIGIN>/#/settings/billing', retry_after: 3600} with Retry-After: 3600. The event is refused, not queued: nothing is stored, so a sender must retry it after reset_at or an upgrade. Only the quota case carries reason, limit, reset_at and upgrade_url; branch on reason, not on the error text. Both limiters fail open if their backing service is unavailable. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer",
                          "description": "seconds: 10 for the edge rate limit, 3600 for the monthly quota"
                        },
                        "upgrade_url": {
                          "type": "string",
                          "format": "uri",
                          "description": "monthly-quota 429 only"
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "quota_exceeded"
                          ],
                          "description": "monthly-quota 429 only: quota_exceeded"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "monthly-quota 429 only: the event count at which ingest refuses (the plan's events plus its soft cap)"
                        },
                        "reset_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "monthly-quota 429 only: when the quota resets, 00:00 UTC on the 1st of next month"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 10 for the edge burst limit, 3600 for the monthly quota. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "error: 'Could not store submission' \u2014 the submissions insert failed with no idempotency winner to fall back to. Also 'handshake_failed' when a Zoom challenge arrives at an endpoint with no secret token (cannot be configured through the API).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The endpoint exists but is disabled: {error: 'This endpoint is disabled, so events sent to it are not being stored. ...', reason: 'endpoint_disabled'}. Nothing is stored and no quota is charged. It is deliberately not the 404 of a wrong URL: providers treat a 404 as a dead endpoint (and may disable it or drop the events), while a 503 is retried, so events sent while it is disabled can still arrive if it is enabled again within the provider's retry window. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "reason": {
                          "type": "string",
                          "enum": [
                            "endpoint_disabled"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "508": {
            "description": "Loop detected: the request's `Hookie-Hop` header says this event has already passed through Hookie 8 times, which almost always means a destination is delivering back into an endpoint that feeds it. Nothing is stored. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The endpoint verifies its provider's signature (#210) and this request's is missing (reason missing_header), does not match the raw body under the endpoint's secret (bad_signature), or carries a timestamp outside the tolerance (stale_timestamp). The body names the header and the scheme; nothing is stored or counted and an ingest_signature_rejected audit row is written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "303": {
            "description": "See Other (ING-8, #193): a browser's native HTML form POST to an endpoint with a redirect_url, once the submission is stored (or recognised as a duplicate). Location is the endpoint's redirect_url.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                },
                "description": "The endpoint's redirect_url."
              }
            }
          }
        },
        "description": "Public webhook URL, app.hookie.ai/{workspace}/{project}/{webhook}. POST, plus OPTIONS (the CORS preflight) and a GET carrying hub.challenge (Meta's URL verification); any other GET, and PUT, fall through to the console and never reach ingest. Only 3- or 4-segment paths match. Every stored submission keeps the request's method, a filtered header map, the query string, the Content-Type and the source IP (ING-3, #193), shown on the record (GET .../records/{id}) and on correlate as `request`. Credentials are never stored: Authorization, Proxy-Authorization, Cookie and any header or query parameter named like a credential (key, api-key, token, secret, password, session, auth) is stored as \"[redacted]\"; Cloudflare and proxy hop headers (cf-*, x-forwarded-*, x-real-ip, true-client-ip, cdn-loop, host) are dropped. HTML forms (ING-8, #193): when the endpoint has a redirect_url, a browser's native form POST (a form encoding with Sec-Fetch-Mode: navigate, or with an Accept that asks for text/html from an older browser) is answered 303 See Other to that URL once stored (a deduplicated retry too); fetch() calls and servers still get the JSON. CORS: when the request's Origin is in the endpoint's cors_origins (or they include \"*\"), every response carries Access-Control-Allow-Origin, Access-Control-Expose-Headers: Retry-After and Vary: Origin. URL verification (ING-10, #193): an endpoint whose handshake is slack, graph, zoom or twitch answers that provider's challenge with 200 before anything is stored or counted - Slack's url_verification body and Twitch's webhook_callback_verification (Twitch-Eventsub-Message-Type header) with the challenge as text/plain, Microsoft Graph's ?validationToken= with the token as text/plain, and Zoom's endpoint.url_validation with {plainToken, encryptedToken} (HMAC-SHA256 of plainToken under the endpoint's secret token). Every other request is an ordinary event.",
        "parameters": [
          {
            "name": "workspace",
            "in": "path",
            "required": true,
            "description": "Workspace (tenant) slug. Must not be one of the reserved roots reserved for app internals (api, admin, stripe, v1, assets, settings, favicon.ico, robots.txt, sitemap.xml, index.html, app.js, styles.css, export.js, portal, brand, trial-info, .well-known, mcp) \u2014 those never reach the ingest handler.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "project",
            "in": "path",
            "required": true,
            "description": "Project slug within the workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook",
            "in": "path",
            "required": true,
            "description": "Webhook slug. This high-entropy slug IS the credential \u2014 no key, token or cookie is sent. A disabled webhook answers 503 rather than 404.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional. If a submission already exists for this tenant with the same key, the request short-circuits with 200 before the monthly quota is charged, so retries are free.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Content-Type selects the parser: 'application/x-www-form-urlencoded' and 'multipart/form-data' are flattened into a shallow object (a field that repeats becomes an array; a file part becomes {filename, type, size} and its BYTES ARE DISCARDED; field names are kept verbatim and flat, so an input named user.email stays the key 'user.email'). ANY other body is parsed as JSON first, whatever its Content-Type (a missing or wrong one included). A body that is not JSON is kept as text when its Content-Type is textual: text/* (plain, csv, xml, ...), application/xml or any +xml type, NDJSON / JSON Lines (application/x-ndjson, application/ndjson, application/jsonl, application/x-jsonlines), application/csv and YAML. It is stored as one event, {\"body\": \"<the text, verbatim>\", \"content_type\": \"<media type, lowercased, parameters stripped>\"}, decoded as UTF-8; NDJSON is not split into several events. A body that does parse as JSON is stored as JSON whatever its Content-Type, exactly as before, so a provider that posts JSON as text/plain still gets an object. A non-JSON body that declares JSON (or no Content-Type) is 400, and one of any other type (application/octet-stream, image/*, ...) is 415. An empty body is accepted and becomes {}. A payload that is not a JSON object (array or scalar) is wrapped as {\"value\": <payload>} before it is stored as a record. Bodies over 1,000,000 bytes are rejected with 413.",
          "content": {
            "application/json": {
              "schema": {
                "description": "Any JSON value. Objects are stored field-for-field; arrays and scalars are wrapped as {value: <payload>}."
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ]
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    {
                      "type": "object",
                      "description": "File part descriptor; the bytes are not stored.",
                      "required": [
                        "filename",
                        "type",
                        "size"
                      ],
                      "properties": {
                        "filename": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "size": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "Plain text, e.g. an alert. Stored as {body, content_type} unless it parses as JSON."
              }
            },
            "application/xml": {
              "schema": {
                "type": "string",
                "description": "XML, e.g. a SOAP outbound message (text/xml and +xml types too). Stored as {body, content_type}."
              }
            },
            "text/csv": {
              "schema": {
                "type": "string",
                "description": "CSV. Stored whole as {body, content_type}; rows are not split."
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "NDJSON / JSON Lines. Stored whole as {body, content_type}; lines are not split into events. A single line that is itself valid JSON is stored as JSON."
              }
            }
          }
        },
        "security": []
      },
      "options": {
        "operationId": "ingestPathFormPreflight",
        "summary": "CORS preflight for a page's fetch() to the public webhook URL",
        "tags": [
          "Ingest"
        ],
        "description": "ING-8 (#193). Answered for an Origin in the endpoint's cors_origins (or when they include \"*\"): 204 with Access-Control-Allow-Origin (the origin, or * for any), Access-Control-Allow-Methods: POST, Access-Control-Allow-Headers: Content-Type, Idempotency-Key, and Access-Control-Max-Age: 600. Any other origin, or an endpoint with no allowed origins (the default), gets 403 and no CORS header, so the browser blocks the call. Nothing is stored or counted. Credentials are never allowed: the URL is the credential.",
        "parameters": [
          {
            "name": "workspace",
            "in": "path",
            "required": true,
            "description": "Workspace (tenant) slug. Must not be one of the reserved roots reserved for app internals (api, admin, stripe, v1, assets, settings, favicon.ico, robots.txt, sitemap.xml, index.html, app.js, styles.css, export.js, portal, brand, trial-info, .well-known, mcp) \u2014 those never reach the ingest handler.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "project",
            "in": "path",
            "required": true,
            "description": "Project slug within the workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook",
            "in": "path",
            "required": true,
            "description": "Webhook slug. This high-entropy slug IS the credential \u2014 no key, token or cookie is sent. A disabled webhook answers 503 rather than 404.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional. If a submission already exists for this tenant with the same key, the request short-circuits with 200 before the monthly quota is charged, so retries are free.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [],
        "responses": {
          "204": {
            "description": "Preflight allowed.",
            "headers": {
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string"
                }
              },
              "Access-Control-Allow-Methods": {
                "schema": {
                  "type": "string",
                  "const": "POST"
                }
              },
              "Access-Control-Allow-Headers": {
                "schema": {
                  "type": "string"
                }
              },
              "Access-Control-Max-Age": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The origin is not allowed (logged as reason cors_origin_not_allowed). Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "error: 'Unknown endpoint'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "ingestPathFormMetaVerification",
        "summary": "Meta's URL verification (hub.challenge) on the public webhook URL",
        "tags": [
          "Ingest"
        ],
        "description": "ING-10 (#193). Reaches ingest only when the query carries hub.challenge; any other GET is the console. For an endpoint whose handshake is meta: with hub.mode=subscribe and a hub.verify_token equal to the endpoint's verify token (handshake_secret), answers 200 text/plain with hub.challenge. A different token is 403 'handshake_failed'. Checked after the endpoint's disabled state, IP allowlists, suspension and burst limit. Nothing is stored or counted.",
        "parameters": [
          {
            "name": "workspace",
            "in": "path",
            "required": true,
            "description": "Workspace (tenant) slug. Must not be one of the reserved roots reserved for app internals (api, admin, stripe, v1, assets, settings, favicon.ico, robots.txt, sitemap.xml, index.html, app.js, styles.css, export.js, portal, brand, trial-info, .well-known, mcp) \u2014 those never reach the ingest handler.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "project",
            "in": "path",
            "required": true,
            "description": "Project slug within the workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook",
            "in": "path",
            "required": true,
            "description": "Webhook slug. This high-entropy slug IS the credential \u2014 no key, token or cookie is sent. A disabled webhook answers 503 rather than 404.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional. If a submission already exists for this tenant with the same key, the request short-circuits with 200 before the monthly quota is charged, so retries are free.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "hub.mode",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "const": "subscribe"
            }
          },
          {
            "name": "hub.challenge",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "maxLength": 2048
            }
          },
          {
            "name": "hub.verify_token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The challenge, verbatim.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "reason 'handshake_failed': no usable hub.challenge. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "reason 'handshake_failed': hub.verify_token does not match. Also 'Source IP not allowed' and workspace_suspended. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "error: 'Unknown endpoint'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "The endpoint does not answer Meta's handshake: 'Use POST'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Burst rate limit (about 100 requests per 10 seconds per endpoint URL; best-effort). Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The endpoint is disabled. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/{workspace}/{project}/{webhook}/{dataset}": {
      "post": {
        "operationId": "ingestPathFormToDataset",
        "summary": "Ingest an event through the public webhook URL, naming its dataset",
        "tags": [
          "Ingest"
        ],
        "responses": {
          "200": {
            "description": "Duplicate - this endpoint (or, on /v1/ingest, this ingest key) already stored a submission with the same dedup key, so the retry is answered with the first submission's id and nothing is stored or counted (ING-9, #193). The dedup key is, in order: an Idempotency-Key header; else a provider delivery id kept the same across that provider's retries - webhook-id (Standard Webhooks), svix-id, X-GitHub-Delivery, X-Shopify-Webhook-Id, X-Gitlab-Event-UUID, I-Twilio-Idempotency-Token, Linear-Delivery, Twitch-Eventsub-Message-Id, X-Atlassian-Webhook-Identifier; else Stripe's event id (a body with object 'event' and an evt_ id) or Slack's event_id (type 'event_callback'). The key space is the ENDPOINT's, not the workspace's: two endpoints may receive the same key. `deduplicated_by` names which source matched. A request that loses the unique-index race has already been counted against the monthly quota. ALSO 200 for an answered URL-verification challenge (ING-10): the challenge as text/plain, or Zoom's JSON {plainToken, encryptedToken}. Nothing is stored or counted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "idempotent"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "idempotent": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "deduplicated_by": {
                      "type": "string",
                      "enum": [
                        "idempotency-key",
                        "standard-webhooks",
                        "svix",
                        "github",
                        "shopify",
                        "gitlab",
                        "twilio",
                        "linear",
                        "twitch",
                        "atlassian",
                        "stripe",
                        "slack"
                      ],
                      "description": "Which dedup source matched."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Stored. {dataset} is the endpoint's own dataset, and the endpoint's own criteria and mappings apply as on the three-segment URL (ING-1); project rules do not run. A match (or empty criteria) makes one record and 'routed' holds that dataset name; a non-match, or a swallowed routing error, is routed: [] and records: 0 with the submission kept. A non-match also carries filtered: true and marks the submission 'filtered' (#287); a routing error does not.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "routed",
                    "records"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "routed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "records": {
                      "type": "integer"
                    },
                    "filtered": {
                      "type": "boolean",
                      "enum": [
                        true
                      ],
                      "description": "Present, and true, only when the endpoint's own criteria rejected the payload (#287). The submission is kept (and counted against the quota) with forward_status 'filtered' and no record, and is listed on the project's Errors tab: GET /admin/api/projects/{project_id}/errors."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body could not be parsed: 'Body must be valid JSON' (the body declares application/json, a +json type or no Content-Type, and does not parse) or 'Body is not valid multipart/form-data'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota. Also reason 'handshake_failed' when the endpoint's URL-verification challenge arrives without a usable challenge or token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The fourth segment names a dataset other than the endpoint's own. Also `reason: \"workspace_suspended\"` when the workspace has been suspended by the operator: nothing is stored, counted or charged, so a sender should retry after the workspace is reinstated. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "error: 'Unknown endpoint' \u2014 unknown workspace/project slug or unknown webhook slug (a disabled one answers 503). After a rotation the previous webhook slug is still accepted until its overlap ends, then it is 404 like any unknown slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "error: 'Body exceeds 1000000 bytes'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The body is not JSON and its Content-Type is neither a form encoding nor a textual type that can be stored as {body, content_type} (for example application/octet-stream or image/png). The error names the Content-Type. Nothing is stored and no quota is charged. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Two cases, each with a Retry-After response header in seconds that equals the retry_after body field. Edge burst limit: about 100 requests per 10 seconds per credential (one endpoint URL, or one ingest key), the same on every plan: {error: 'Rate limit exceeded', retry_after: 10} with Retry-After: 10. It is a best-effort burst guard, not an exact count: it is counted separately at each Cloudflare location and catches up within seconds, so a short burst can get through above it. Monthly plan quota, the exact count (the plan's events plus its soft cap: +10% on Pro and Team, none on Free): {error: 'Monthly event quota reached: this event was not stored. The quota resets at <reset_at>; upgrade the plan to raise it sooner.', reason: 'quota_exceeded', limit: <events at which ingest refuses>, reset_at: '<00:00 UTC on the 1st of next month>', upgrade_url: '<APP_ORIGIN>/#/settings/billing', retry_after: 3600} with Retry-After: 3600. The event is refused, not queued: nothing is stored, so a sender must retry it after reset_at or an upgrade. Only the quota case carries reason, limit, reset_at and upgrade_url; branch on reason, not on the error text. Both limiters fail open if their backing service is unavailable. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer"
                        },
                        "upgrade_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "quota_exceeded"
                          ],
                          "description": "monthly-quota 429 only: quota_exceeded"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "monthly-quota 429 only: the event count at which ingest refuses (the plan's events plus its soft cap)"
                        },
                        "reset_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "monthly-quota 429 only: when the quota resets, 00:00 UTC on the 1st of next month"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 10 for the edge burst limit, 3600 for the monthly quota. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "error: 'Could not store submission' \u2014 the submissions insert failed with no idempotency winner to fall back to. Also 'handshake_failed' when a Zoom challenge arrives at an endpoint with no secret token (cannot be configured through the API).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The endpoint exists but is disabled: {error: 'This endpoint is disabled, so events sent to it are not being stored. ...', reason: 'endpoint_disabled'}. Nothing is stored and no quota is charged. It is deliberately not the 404 of a wrong URL: providers treat a 404 as a dead endpoint (and may disable it or drop the events), while a 503 is retried, so events sent while it is disabled can still arrive if it is enabled again within the provider's retry window. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "reason": {
                          "type": "string",
                          "enum": [
                            "endpoint_disabled"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "508": {
            "description": "Loop detected: the request's `Hookie-Hop` header says this event has already passed through Hookie 8 times, which almost always means a destination is delivering back into an endpoint that feeds it. Nothing is stored. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The endpoint verifies its provider's signature (#210) and this request's is missing (reason missing_header), does not match the raw body under the endpoint's secret (bad_signature), or carries a timestamp outside the tolerance (stale_timestamp). The body names the header and the scheme; nothing is stored or counted and an ingest_signature_rejected audit row is written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "303": {
            "description": "See Other (ING-8, #193): a browser's native HTML form POST to an endpoint with a redirect_url, once the submission is stored (or recognised as a duplicate). Location is the endpoint's redirect_url.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                },
                "description": "The endpoint's redirect_url."
              }
            }
          }
        },
        "description": "Four-segment form of the public webhook URL. POST, plus the OPTIONS preflight and Meta's hub.challenge GET, which behave as on the three-segment URL; other methods fall through to the console. The fourth segment must be the endpoint's OWN dataset: any other value is refused with 403. It used to override the dataset with any dataset in the project, which let anyone holding a public endpoint URL (one embedded in an HTML form, say) write into every dataset in the project and set off its destinations, triggers and workflows. To route one source into several datasets, use an ingest key: POST /v1/ingest/{ingest_key}/{dataset}. Every stored submission keeps the request's method, a filtered header map, the query string, the Content-Type and the source IP (ING-3, #193), shown on the record (GET .../records/{id}) and on correlate as `request`. Credentials are never stored: Authorization, Proxy-Authorization, Cookie and any header or query parameter named like a credential (key, api-key, token, secret, password, session, auth) is stored as \"[redacted]\"; Cloudflare and proxy hop headers (cf-*, x-forwarded-*, x-real-ip, true-client-ip, cdn-loop, host) are dropped. HTML forms (ING-8, #193): when the endpoint has a redirect_url, a browser's native form POST (a form encoding with Sec-Fetch-Mode: navigate, or with an Accept that asks for text/html from an older browser) is answered 303 See Other to that URL once stored (a deduplicated retry too); fetch() calls and servers still get the JSON. CORS: when the request's Origin is in the endpoint's cors_origins (or they include \"*\"), every response carries Access-Control-Allow-Origin, Access-Control-Expose-Headers: Retry-After and Vary: Origin. URL verification (ING-10, #193): an endpoint whose handshake is slack, graph, zoom or twitch answers that provider's challenge with 200 before anything is stored or counted - Slack's url_verification body and Twitch's webhook_callback_verification (Twitch-Eventsub-Message-Type header) with the challenge as text/plain, Microsoft Graph's ?validationToken= with the token as text/plain, and Zoom's endpoint.url_validation with {plainToken, encryptedToken} (HMAC-SHA256 of plainToken under the endpoint's secret token). Every other request is an ordinary event.",
        "parameters": [
          {
            "name": "workspace",
            "in": "path",
            "required": true,
            "description": "Workspace (tenant) slug; must not be a reserved root (api, admin, stripe, v1, assets, settings, favicon.ico, robots.txt, sitemap.xml, index.html, app.js, styles.css, export.js, portal, brand, trial-info, .well-known, mcp).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "project",
            "in": "path",
            "required": true,
            "description": "Project slug within the workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "webhook",
            "in": "path",
            "required": true,
            "description": "Webhook slug \u2014 the credential. A disabled one answers 503.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dataset",
            "in": "path",
            "required": true,
            "description": "Dataset to store the record in, overriding the webhook's configured dataset for this request. No validation is performed; the name is used as given.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional. A repeat key short-circuits with 200 before quota is charged.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Same parsing as the 3-segment form: urlencoded and multipart are flattened (repeats become arrays, file parts become {filename, type, size} with the bytes discarded); every other body is parsed as JSON first, and textual non-JSON bodies are stored as {body, content_type} (see ingestPathForm); an empty body becomes {}; a non-object payload is wrapped as {\"value\": <payload>}. Over 1,000,000 bytes yields 413.",
          "content": {
            "application/json": {
              "schema": {
                "description": "Any JSON value."
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ]
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "filename",
                        "type",
                        "size"
                      ],
                      "properties": {
                        "filename": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "size": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "Plain text, e.g. an alert. Stored as {body, content_type} unless it parses as JSON."
              }
            },
            "application/xml": {
              "schema": {
                "type": "string",
                "description": "XML, e.g. a SOAP outbound message (text/xml and +xml types too). Stored as {body, content_type}."
              }
            },
            "text/csv": {
              "schema": {
                "type": "string",
                "description": "CSV. Stored whole as {body, content_type}; rows are not split."
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "NDJSON / JSON Lines. Stored whole as {body, content_type}; lines are not split into events. A single line that is itself valid JSON is stored as JSON."
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/ingest/{ingest_key}": {
      "post": {
        "operationId": "ingestEvent",
        "summary": "Ingest an event with an ingest key, routed by the project's mapping rules, with the key's default dataset as the fallback",
        "tags": [
          "Ingest"
        ],
        "responses": {
          "200": {
            "description": "Duplicate - this endpoint (or, on /v1/ingest, this ingest key) already stored a submission with the same dedup key, so the retry is answered with the first submission's id and nothing is stored or counted (ING-9, #193). The dedup key is, in order: an Idempotency-Key header; else a provider delivery id kept the same across that provider's retries - webhook-id (Standard Webhooks), svix-id, X-GitHub-Delivery, X-Shopify-Webhook-Id, X-Gitlab-Event-UUID, I-Twilio-Idempotency-Token, Linear-Delivery, Twitch-Eventsub-Message-Id, X-Atlassian-Webhook-Identifier; else Stripe's event id (a body with object 'event' and an evt_ id) or Slack's event_id (type 'event_callback'). The key space is the ENDPOINT's, not the workspace's: two endpoints may receive the same key. `deduplicated_by` names which source matched. A request that loses the unique-index race has already been counted against the monthly quota.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "idempotent"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "idempotent": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "deduplicated_by": {
                      "type": "string",
                      "enum": [
                        "idempotency-key",
                        "standard-webhooks",
                        "svix",
                        "github",
                        "shopify",
                        "gitlab",
                        "twilio",
                        "linear",
                        "twitch",
                        "atlassian",
                        "stripe",
                        "slack"
                      ],
                      "description": "Which dedup source matched."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Stored. With no dataset in the path the payload runs through the project's enabled mapping rules (webhook mirror rules excluded), and 'routed' holds the NAME of every rule that matched and applied (fallback: false). When no rule produces a record (none matched, or every matching rule failed to write), the whole payload is stored as one record in the key's dataset_default instead (#280): routed: [\"<that dataset>\"], records: 1, fallback: true. That record fans out (deliveries, live stream, AI triggers, workflows) exactly as a rule-routed one does, and the request is charged the same one quota event either way. A routing failure the fallback cannot recover from is still a 201, with routed: [] and records: 0; the raw submission is always kept and marked failed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "routed",
                    "records",
                    "fallback"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "routed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "names of the mapping rules that applied; with fallback: true, the single dataset the event was stored in instead"
                    },
                    "records": {
                      "type": "integer",
                      "description": "routed.length"
                    },
                    "fallback": {
                      "type": "boolean",
                      "description": "True when no mapping rule routed the event and it was stored whole, as one record, in the ingest key's dataset_default (#280); 'routed' then holds that dataset's name instead of rule names. False when rules routed it. Present only on this route (no dataset segment)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body could not be parsed: 'Body must be valid JSON' (the body declares application/json, a +json type or no Content-Type, and does not parse) or 'Body is not valid multipart/form-data'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Only when the key has require_signature: 'Signature required' (header absent or no stored secret) or 'Invalid signature'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "error: 'Source IP not allowed'. The Cloudflare client IP must satisfy BOTH the key's allowlist and the tenant's (an empty allowlist matches everything); the block is audited. Also `reason: \"workspace_suspended\"` when the workspace has been suspended by the operator: nothing is stored, counted or charged, so a sender should retry after the workspace is reinstated. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "error: 'Unknown ingest key' \u2014 no live key with this prefix (revoked, or past its expires_at), hash mismatch, or the key's tenant row is missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "error: 'Body exceeds 1000000 bytes' \u2014 from Content-Length, or from the buffered byte length. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The body is not JSON and its Content-Type is neither a form encoding nor a textual type that can be stored as {body, content_type} (for example application/octet-stream or image/png). The error names the Content-Type. Nothing is stored and no quota is charged. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Two cases, each with a Retry-After response header in seconds that equals the retry_after body field. Edge burst limit: about 100 requests per 10 seconds per credential (one endpoint URL, or one ingest key), the same on every plan: {error: 'Rate limit exceeded', retry_after: 10} with Retry-After: 10. It is a best-effort burst guard, not an exact count: it is counted separately at each Cloudflare location and catches up within seconds, so a short burst can get through above it. Monthly plan quota, the exact count (the plan's events plus its soft cap: +10% on Pro and Team, none on Free): {error: 'Monthly event quota reached: this event was not stored. The quota resets at <reset_at>; upgrade the plan to raise it sooner.', reason: 'quota_exceeded', limit: <events at which ingest refuses>, reset_at: '<00:00 UTC on the 1st of next month>', upgrade_url: '<APP_ORIGIN>/#/settings/billing', retry_after: 3600} with Retry-After: 3600. The event is refused, not queued: nothing is stored, so a sender must retry it after reset_at or an upgrade. Only the quota case carries reason, limit, reset_at and upgrade_url; branch on reason, not on the error text. Both limiters fail open if their backing service is unavailable. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer",
                          "description": "10 (rate limit) or 3600 (quota)"
                        },
                        "upgrade_url": {
                          "type": "string",
                          "format": "uri",
                          "description": "quota 429 only"
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "quota_exceeded"
                          ],
                          "description": "monthly-quota 429 only: quota_exceeded"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "monthly-quota 429 only: the event count at which ingest refuses (the plan's events plus its soft cap)"
                        },
                        "reset_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "monthly-quota 429 only: when the quota resets, 00:00 UTC on the 1st of next month"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 10 for the edge burst limit, 3600 for the monthly quota. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "error: 'Key misconfigured' (signing secret could not be decrypted) or 'Could not store submission'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "508": {
            "description": "Loop detected: the request's `Hookie-Hop` header says this event has already passed through Hookie 8 times, which almost always means a destination is delivering back into an endpoint that feeds it. Nothing is stored. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Generic ingest. PUT is accepted on this same path and behaves identically (see ingestEventPut); every other method returns 405 {\"error\":\"Use POST\"}, including OPTIONS \u2014 there are no CORS headers on this route. Every stored submission keeps the request's method, a filtered header map, the query string, the Content-Type and the source IP (ING-3, #193), shown on the record (GET .../records/{id}) and on correlate as `request`. Credentials are never stored: Authorization, Proxy-Authorization, Cookie and any header or query parameter named like a credential (key, api-key, token, secret, password, session, auth) is stored as \"[redacted]\"; Cloudflare and proxy hop headers (cf-*, x-forwarded-*, x-real-ip, true-client-ip, cdn-loop, host) are dropped.",
        "parameters": [
          {
            "name": "ingest_key",
            "in": "path",
            "required": true,
            "description": "The plaintext ingest key. It is looked up by its full SHA-256 hash (#177), among keys that are neither revoked nor expired, and compared constant-time; key_prefix is only a display label.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional. If a submission with this key already exists for the tenant, the request short-circuits with 200 before the monthly quota is charged.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Hookie-Signature",
            "in": "header",
            "required": false,
            "description": "Required only when the ingest key has require_signature set; ignored otherwise. Format: 't=<unix-timestamp>,v1=<hex>' where v1 is HMAC-SHA-256 over '<t>.<raw body>' keyed with the key's signing secret.",
            "schema": {
              "type": "string",
              "example": "t=1700000000,v1=3ba8c0e9..."
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Content-Type selects the parser: 'application/x-www-form-urlencoded' and 'multipart/form-data' are flattened into a shallow object (repeated fields become arrays; file parts become {filename, type, size} and their bytes are discarded). EVERY other body is parsed as JSON first, whatever its Content-Type (missing or wrong ones included, which is deliberate so webhook providers that send JSON with an unhelpful Content-Type still work). A body that is not JSON is kept as text when its Content-Type is textual: text/* (plain, csv, xml, ...), application/xml or any +xml type, NDJSON / JSON Lines (application/x-ndjson, application/ndjson, application/jsonl, application/x-jsonlines), application/csv and YAML. It is stored as one event, {\"body\": \"<the text, verbatim>\", \"content_type\": \"<media type, lowercased, parameters stripped>\"}, decoded as UTF-8; NDJSON is not split into several events. A body that does parse as JSON is stored as JSON whatever its Content-Type, exactly as before, so a provider that posts JSON as text/plain still gets an object. A non-JSON body that declares JSON (or no Content-Type) is 400, and one of any other type (application/octet-stream, image/*, ...) is 415. An empty body is accepted and becomes {}. Over 1,000,000 bytes yields 413.",
          "content": {
            "application/json": {
              "schema": {
                "description": "Any JSON value. On this route the payload is fed to the project's mapping rules, so its shape is whatever the rules' conditions and mappings expect."
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ]
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "filename",
                        "type",
                        "size"
                      ],
                      "properties": {
                        "filename": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "size": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "Plain text, e.g. an alert. Stored as {body, content_type} unless it parses as JSON."
              }
            },
            "application/xml": {
              "schema": {
                "type": "string",
                "description": "XML, e.g. a SOAP outbound message (text/xml and +xml types too). Stored as {body, content_type}."
              }
            },
            "text/csv": {
              "schema": {
                "type": "string",
                "description": "CSV. Stored whole as {body, content_type}; rows are not split."
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "NDJSON / JSON Lines. Stored whole as {body, content_type}; lines are not split into events. A single line that is itself valid JSON is stored as JSON."
              }
            }
          }
        },
        "security": [
          {
            "ingestKey": []
          }
        ]
      },
      "put": {
        "operationId": "ingestEventPut",
        "summary": "Ingest an event with an ingest key (PUT alias of ingestEvent)",
        "tags": [
          "Ingest"
        ],
        "responses": {
          "200": {
            "description": "Duplicate - this endpoint (or, on /v1/ingest, this ingest key) already stored a submission with the same dedup key, so the retry is answered with the first submission's id and nothing is stored or counted (ING-9, #193). The dedup key is, in order: an Idempotency-Key header; else a provider delivery id kept the same across that provider's retries - webhook-id (Standard Webhooks), svix-id, X-GitHub-Delivery, X-Shopify-Webhook-Id, X-Gitlab-Event-UUID, I-Twilio-Idempotency-Token, Linear-Delivery, Twitch-Eventsub-Message-Id, X-Atlassian-Webhook-Identifier; else Stripe's event id (a body with object 'event' and an evt_ id) or Slack's event_id (type 'event_callback'). The key space is the ENDPOINT's, not the workspace's: two endpoints may receive the same key. `deduplicated_by` names which source matched. A request that loses the unique-index race has already been counted against the monthly quota.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "idempotent"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "idempotent": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "deduplicated_by": {
                      "type": "string",
                      "enum": [
                        "idempotency-key",
                        "standard-webhooks",
                        "svix",
                        "github",
                        "shopify",
                        "gitlab",
                        "twilio",
                        "linear",
                        "twitch",
                        "atlassian",
                        "stripe",
                        "slack"
                      ],
                      "description": "Which dedup source matched."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Stored; 'routed' holds the names of the mapping rules that applied or, when none routed it and fallback is true, the key's dataset_default the whole payload was stored in (#280).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "routed",
                    "records",
                    "fallback"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "routed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "records": {
                      "type": "integer"
                    },
                    "fallback": {
                      "type": "boolean",
                      "description": "True when no mapping rule routed the event and it was stored whole, as one record, in the ingest key's dataset_default (#280); 'routed' then holds that dataset's name instead of rule names. False when rules routed it. Present only on this route (no dataset segment)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body could not be parsed: 'Body must be valid JSON' (the body declares application/json, a +json type or no Content-Type, and does not parse) or 'Body is not valid multipart/form-data'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "'Signature required' or 'Invalid signature' (signature-required keys only). Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "error: 'Source IP not allowed'. Also `reason: \"workspace_suspended\"` when the workspace has been suspended by the operator: nothing is stored, counted or charged, so a sender should retry after the workspace is reinstated. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "error: 'Unknown ingest key'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "error: 'Body exceeds 1000000 bytes'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The body is not JSON and its Content-Type is neither a form encoding nor a textual type that can be stored as {body, content_type} (for example application/octet-stream or image/png). The error names the Content-Type. Nothing is stored and no quota is charged. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Two cases, each with a Retry-After response header in seconds that equals the retry_after body field. Edge burst limit: about 100 requests per 10 seconds per credential (one endpoint URL, or one ingest key), the same on every plan: {error: 'Rate limit exceeded', retry_after: 10} with Retry-After: 10. It is a best-effort burst guard, not an exact count: it is counted separately at each Cloudflare location and catches up within seconds, so a short burst can get through above it. Monthly plan quota, the exact count (the plan's events plus its soft cap: +10% on Pro and Team, none on Free): {error: 'Monthly event quota reached: this event was not stored. The quota resets at <reset_at>; upgrade the plan to raise it sooner.', reason: 'quota_exceeded', limit: <events at which ingest refuses>, reset_at: '<00:00 UTC on the 1st of next month>', upgrade_url: '<APP_ORIGIN>/#/settings/billing', retry_after: 3600} with Retry-After: 3600. The event is refused, not queued: nothing is stored, so a sender must retry it after reset_at or an upgrade. Only the quota case carries reason, limit, reset_at and upgrade_url; branch on reason, not on the error text. Both limiters fail open if their backing service is unavailable. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer"
                        },
                        "upgrade_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "quota_exceeded"
                          ],
                          "description": "monthly-quota 429 only: quota_exceeded"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "monthly-quota 429 only: the event count at which ingest refuses (the plan's events plus its soft cap)"
                        },
                        "reset_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "monthly-quota 429 only: when the quota resets, 00:00 UTC on the 1st of next month"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 10 for the edge burst limit, 3600 for the monthly quota. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "error: 'Key misconfigured' or 'Could not store submission'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "508": {
            "description": "Loop detected: the request's `Hookie-Hop` header says this event has already passed through Hookie 8 times, which almost always means a destination is delivering back into an endpoint that feeds it. Nothing is stored. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "PUT is accepted verbatim \u2014 the handler's only method check allows POST and PUT, and nothing downstream branches on the method. Not idempotent in the HTTP sense: each PUT stores a new submission unless an Idempotency-Key repeats. Every stored submission keeps the request's method, a filtered header map, the query string, the Content-Type and the source IP (ING-3, #193), shown on the record (GET .../records/{id}) and on correlate as `request`. Credentials are never stored: Authorization, Proxy-Authorization, Cookie and any header or query parameter named like a credential (key, api-key, token, secret, password, session, auth) is stored as \"[redacted]\"; Cloudflare and proxy hop headers (cf-*, x-forwarded-*, x-real-ip, true-client-ip, cdn-loop, host) are dropped.",
        "parameters": [
          {
            "name": "ingest_key",
            "in": "path",
            "required": true,
            "description": "The plaintext ingest key (looked up by its full SHA-256 hash, #177; revoked and expired keys never match).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional; a repeat key short-circuits with 200 before quota is charged.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Hookie-Signature",
            "in": "header",
            "required": false,
            "description": "Required only when the key has require_signature. Format 't=<unix-timestamp>,v1=<hex>', v1 = HMAC-SHA-256 over '<t>.<raw body>'.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Identical to POST /v1/ingest/{ingest_key}: form encodings are flattened, anything else is parsed as JSON first and textual non-JSON bodies are stored as {body, content_type}, an empty body becomes {}, and bodies over 1,000,000 bytes are rejected with 413.",
          "content": {
            "application/json": {
              "schema": {
                "description": "Any JSON value."
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ]
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "filename",
                        "type",
                        "size"
                      ],
                      "properties": {
                        "filename": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "size": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "Plain text, e.g. an alert. Stored as {body, content_type} unless it parses as JSON."
              }
            },
            "application/xml": {
              "schema": {
                "type": "string",
                "description": "XML, e.g. a SOAP outbound message (text/xml and +xml types too). Stored as {body, content_type}."
              }
            },
            "text/csv": {
              "schema": {
                "type": "string",
                "description": "CSV. Stored whole as {body, content_type}; rows are not split."
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "NDJSON / JSON Lines. Stored whole as {body, content_type}; lines are not split into events. A single line that is itself valid JSON is stored as JSON."
              }
            }
          }
        },
        "security": [
          {
            "ingestKey": []
          }
        ]
      }
    },
    "/v1/ingest/{ingest_key}/{dataset}": {
      "post": {
        "operationId": "ingestEventToDataset",
        "summary": "Ingest an event with an ingest key straight into a named dataset",
        "tags": [
          "Ingest"
        ],
        "responses": {
          "200": {
            "description": "Duplicate - this endpoint (or, on /v1/ingest, this ingest key) already stored a submission with the same dedup key, so the retry is answered with the first submission's id and nothing is stored or counted (ING-9, #193). The dedup key is, in order: an Idempotency-Key header; else a provider delivery id kept the same across that provider's retries - webhook-id (Standard Webhooks), svix-id, X-GitHub-Delivery, X-Shopify-Webhook-Id, X-Gitlab-Event-UUID, I-Twilio-Idempotency-Token, Linear-Delivery, Twitch-Eventsub-Message-Id, X-Atlassian-Webhook-Identifier; else Stripe's event id (a body with object 'event' and an evt_ id) or Slack's event_id (type 'event_callback'). The key space is the ENDPOINT's, not the workspace's: two endpoints may receive the same key. `deduplicated_by` names which source matched. A request that loses the unique-index race has already been counted against the monthly quota.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "idempotent"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "idempotent": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "deduplicated_by": {
                      "type": "string",
                      "enum": [
                        "idempotency-key",
                        "standard-webhooks",
                        "svix",
                        "github",
                        "shopify",
                        "gitlab",
                        "twilio",
                        "linear",
                        "twitch",
                        "atlassian",
                        "stripe",
                        "slack"
                      ],
                      "description": "Which dedup source matched."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Stored and identity-routed; 'routed' is the single-element array holding {dataset}. A swallowed routing failure surfaces as routed: [] and records: 0.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "routed",
                    "records"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "routed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "records": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body could not be parsed: 'Body must be valid JSON' (the body declares application/json, a +json type or no Content-Type, and does not parse) or 'Body is not valid multipart/form-data'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Signature-required keys only: 'Signature required' or 'Invalid signature'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "error: 'Source IP not allowed' \u2014 key allowlist AND tenant allowlist must both match. Also `reason: \"workspace_suspended\"` when the workspace has been suspended by the operator: nothing is stored, counted or charged, so a sender should retry after the workspace is reinstated. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "error: 'Unknown ingest key' \u2014 unknown/revoked key, hash mismatch, or missing tenant row. An unknown dataset name is NOT a 404; the dataset is created implicitly by the write.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "error: 'Body exceeds 1000000 bytes'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The body is not JSON and its Content-Type is neither a form encoding nor a textual type that can be stored as {body, content_type} (for example application/octet-stream or image/png). The error names the Content-Type. Nothing is stored and no quota is charged. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Two cases, each with a Retry-After response header in seconds that equals the retry_after body field. Edge burst limit: about 100 requests per 10 seconds per credential (one endpoint URL, or one ingest key), the same on every plan: {error: 'Rate limit exceeded', retry_after: 10} with Retry-After: 10. It is a best-effort burst guard, not an exact count: it is counted separately at each Cloudflare location and catches up within seconds, so a short burst can get through above it. Monthly plan quota, the exact count (the plan's events plus its soft cap: +10% on Pro and Team, none on Free): {error: 'Monthly event quota reached: this event was not stored. The quota resets at <reset_at>; upgrade the plan to raise it sooner.', reason: 'quota_exceeded', limit: <events at which ingest refuses>, reset_at: '<00:00 UTC on the 1st of next month>', upgrade_url: '<APP_ORIGIN>/#/settings/billing', retry_after: 3600} with Retry-After: 3600. The event is refused, not queued: nothing is stored, so a sender must retry it after reset_at or an upgrade. Only the quota case carries reason, limit, reset_at and upgrade_url; branch on reason, not on the error text. Both limiters fail open if their backing service is unavailable. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer"
                        },
                        "upgrade_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "quota_exceeded"
                          ],
                          "description": "monthly-quota 429 only: quota_exceeded"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "monthly-quota 429 only: the event count at which ingest refuses (the plan's events plus its soft cap)"
                        },
                        "reset_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "monthly-quota 429 only: when the quota resets, 00:00 UTC on the 1st of next month"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 10 for the edge burst limit, 3600 for the monthly quota. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "error: 'Key misconfigured' or 'Could not store submission'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "508": {
            "description": "Loop detected: the request's `Hookie-Hop` header says this event has already passed through Hookie 8 times, which almost always means a destination is delivering back into an endpoint that feeds it. Nothing is stored. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Same pipeline as ingestEvent but with identity routing into {dataset}. PUT is accepted identically (ingestEventToDatasetPut); any other method returns 405 {\"error\":\"Use POST\"}. Every stored submission keeps the request's method, a filtered header map, the query string, the Content-Type and the source IP (ING-3, #193), shown on the record (GET .../records/{id}) and on correlate as `request`. Credentials are never stored: Authorization, Proxy-Authorization, Cookie and any header or query parameter named like a credential (key, api-key, token, secret, password, session, auth) is stored as \"[redacted]\"; Cloudflare and proxy hop headers (cf-*, x-forwarded-*, x-real-ip, true-client-ip, cdn-loop, host) are dropped.",
        "parameters": [
          {
            "name": "ingest_key",
            "in": "path",
            "required": true,
            "description": "The plaintext ingest key (looked up by its full SHA-256 hash, #177; revoked and expired keys never match).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dataset",
            "in": "path",
            "required": true,
            "description": "Dataset name. Its presence switches routing to identity: the whole payload is stored as one record in this dataset and the project's mapping rules are NOT consulted. The name is used as given (no validation, no lookup). The key's dataset_default is not used here: it is the fallback only when the path names no dataset.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional; a repeat key short-circuits with 200 before quota is charged.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Hookie-Signature",
            "in": "header",
            "required": false,
            "description": "Required only when the key has require_signature. Format 't=<unix-timestamp>,v1=<hex>', v1 = HMAC-SHA-256 over '<t>.<raw body>'.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Form encodings are flattened (repeats become arrays, file parts become {filename, type, size} with the bytes discarded); every other body is parsed as JSON first, and textual non-JSON bodies are stored as {body, content_type} (see ingestEvent); an empty body becomes {}. On this route a payload that is not a JSON object (array or scalar) is wrapped as {\"value\": <payload>} before it is stored. Over 1,000,000 bytes yields 413.",
          "content": {
            "application/json": {
              "schema": {
                "description": "Any JSON value; objects are stored field-for-field, arrays and scalars are wrapped as {value: <payload>}."
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ]
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "filename",
                        "type",
                        "size"
                      ],
                      "properties": {
                        "filename": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "size": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "Plain text, e.g. an alert. Stored as {body, content_type} unless it parses as JSON."
              }
            },
            "application/xml": {
              "schema": {
                "type": "string",
                "description": "XML, e.g. a SOAP outbound message (text/xml and +xml types too). Stored as {body, content_type}."
              }
            },
            "text/csv": {
              "schema": {
                "type": "string",
                "description": "CSV. Stored whole as {body, content_type}; rows are not split."
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "NDJSON / JSON Lines. Stored whole as {body, content_type}; lines are not split into events. A single line that is itself valid JSON is stored as JSON."
              }
            }
          }
        },
        "security": [
          {
            "ingestKey": []
          }
        ]
      },
      "put": {
        "operationId": "ingestEventToDatasetPut",
        "summary": "Ingest an event into a named dataset (PUT alias of ingestEventToDataset)",
        "tags": [
          "Ingest"
        ],
        "responses": {
          "200": {
            "description": "Duplicate - this endpoint (or, on /v1/ingest, this ingest key) already stored a submission with the same dedup key, so the retry is answered with the first submission's id and nothing is stored or counted (ING-9, #193). The dedup key is, in order: an Idempotency-Key header; else a provider delivery id kept the same across that provider's retries - webhook-id (Standard Webhooks), svix-id, X-GitHub-Delivery, X-Shopify-Webhook-Id, X-Gitlab-Event-UUID, I-Twilio-Idempotency-Token, Linear-Delivery, Twitch-Eventsub-Message-Id, X-Atlassian-Webhook-Identifier; else Stripe's event id (a body with object 'event' and an evt_ id) or Slack's event_id (type 'event_callback'). The key space is the ENDPOINT's, not the workspace's: two endpoints may receive the same key. `deduplicated_by` names which source matched. A request that loses the unique-index race has already been counted against the monthly quota.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "idempotent"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "idempotent": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "deduplicated_by": {
                      "type": "string",
                      "enum": [
                        "idempotency-key",
                        "standard-webhooks",
                        "svix",
                        "github",
                        "shopify",
                        "gitlab",
                        "twilio",
                        "linear",
                        "twitch",
                        "atlassian",
                        "stripe",
                        "slack"
                      ],
                      "description": "Which dedup source matched."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Stored and identity-routed into {dataset}.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "routed",
                    "records"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "routed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "records": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body could not be parsed: 'Body must be valid JSON' (the body declares application/json, a +json type or no Content-Type, and does not parse) or 'Body is not valid multipart/form-data'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "'Signature required' or 'Invalid signature'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "error: 'Source IP not allowed'. Also `reason: \"workspace_suspended\"` when the workspace has been suspended by the operator: nothing is stored, counted or charged, so a sender should retry after the workspace is reinstated. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "error: 'Unknown ingest key'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "error: 'Body exceeds 1000000 bytes'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The body is not JSON and its Content-Type is neither a form encoding nor a textual type that can be stored as {body, content_type} (for example application/octet-stream or image/png). The error names the Content-Type. Nothing is stored and no quota is charged. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Two cases, each with a Retry-After response header in seconds that equals the retry_after body field. Edge burst limit: about 100 requests per 10 seconds per credential (one endpoint URL, or one ingest key), the same on every plan: {error: 'Rate limit exceeded', retry_after: 10} with Retry-After: 10. It is a best-effort burst guard, not an exact count: it is counted separately at each Cloudflare location and catches up within seconds, so a short burst can get through above it. Monthly plan quota, the exact count (the plan's events plus its soft cap: +10% on Pro and Team, none on Free): {error: 'Monthly event quota reached: this event was not stored. The quota resets at <reset_at>; upgrade the plan to raise it sooner.', reason: 'quota_exceeded', limit: <events at which ingest refuses>, reset_at: '<00:00 UTC on the 1st of next month>', upgrade_url: '<APP_ORIGIN>/#/settings/billing', retry_after: 3600} with Retry-After: 3600. The event is refused, not queued: nothing is stored, so a sender must retry it after reset_at or an upgrade. Only the quota case carries reason, limit, reset_at and upgrade_url; branch on reason, not on the error text. Both limiters fail open if their backing service is unavailable. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer"
                        },
                        "upgrade_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "quota_exceeded"
                          ],
                          "description": "monthly-quota 429 only: quota_exceeded"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "monthly-quota 429 only: the event count at which ingest refuses (the plan's events plus its soft cap)"
                        },
                        "reset_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "monthly-quota 429 only: when the quota resets, 00:00 UTC on the 1st of next month"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 10 for the edge burst limit, 3600 for the monthly quota. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "error: 'Key misconfigured' or 'Could not store submission'.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "508": {
            "description": "Loop detected: the request's `Hookie-Hop` header says this event has already passed through Hookie 8 times, which almost always means a destination is delivering back into an endpoint that feeds it. Nothing is stored. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "PUT alias; behaviour is byte-for-byte the same as the POST operation. Every stored submission keeps the request's method, a filtered header map, the query string, the Content-Type and the source IP (ING-3, #193), shown on the record (GET .../records/{id}) and on correlate as `request`. Credentials are never stored: Authorization, Proxy-Authorization, Cookie and any header or query parameter named like a credential (key, api-key, token, secret, password, session, auth) is stored as \"[redacted]\"; Cloudflare and proxy hop headers (cf-*, x-forwarded-*, x-real-ip, true-client-ip, cdn-loop, host) are dropped.",
        "parameters": [
          {
            "name": "ingest_key",
            "in": "path",
            "required": true,
            "description": "The plaintext ingest key.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dataset",
            "in": "path",
            "required": true,
            "description": "Dataset name; forces identity routing (mapping rules are not consulted).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional; a repeat key short-circuits with 200 before quota is charged.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Hookie-Signature",
            "in": "header",
            "required": false,
            "description": "Required only when the key has require_signature. Format 't=<unix-timestamp>,v1=<hex>'.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Identical to the POST form: form encodings flattened, everything else parsed as JSON first with textual non-JSON bodies stored as {body, content_type}, empty body becomes {}, non-object payloads wrapped as {\"value\": <payload>}, 1,000,000-byte cap.",
          "content": {
            "application/json": {
              "schema": {
                "description": "Any JSON value."
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ]
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "filename",
                        "type",
                        "size"
                      ],
                      "properties": {
                        "filename": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "size": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "Plain text, e.g. an alert. Stored as {body, content_type} unless it parses as JSON."
              }
            },
            "application/xml": {
              "schema": {
                "type": "string",
                "description": "XML, e.g. a SOAP outbound message (text/xml and +xml types too). Stored as {body, content_type}."
              }
            },
            "text/csv": {
              "schema": {
                "type": "string",
                "description": "CSV. Stored whole as {body, content_type}; rows are not split."
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "NDJSON / JSON Lines. Stored whole as {body, content_type}; lines are not split into events. A single line that is itself valid JSON is stored as JSON."
              }
            }
          }
        },
        "security": [
          {
            "ingestKey": []
          }
        ]
      }
    },
    "/v1/webhooks/{ingest_key}/{slug}": {
      "post": {
        "operationId": "ingestWebhookEvent",
        "summary": "Ingest an event into a configured endpoint using an ingest key",
        "tags": [
          "Ingest"
        ],
        "responses": {
          "200": {
            "description": "Duplicate - this endpoint (or, on /v1/ingest, this ingest key) already stored a submission with the same dedup key, so the retry is answered with the first submission's id and nothing is stored or counted (ING-9, #193). The dedup key is, in order: an Idempotency-Key header; else a provider delivery id kept the same across that provider's retries - webhook-id (Standard Webhooks), svix-id, X-GitHub-Delivery, X-Shopify-Webhook-Id, X-Gitlab-Event-UUID, I-Twilio-Idempotency-Token, Linear-Delivery, Twitch-Eventsub-Message-Id, X-Atlassian-Webhook-Identifier; else Stripe's event id (a body with object 'event' and an evt_ id) or Slack's event_id (type 'event_callback'). The key space is the ENDPOINT's, not the workspace's: two endpoints may receive the same key. `deduplicated_by` names which source matched. A request that loses the unique-index race has already been counted against the monthly quota.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "idempotent"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "idempotent": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "deduplicated_by": {
                      "type": "string",
                      "enum": [
                        "idempotency-key",
                        "standard-webhooks",
                        "svix",
                        "github",
                        "shopify",
                        "gitlab",
                        "twilio",
                        "linear",
                        "twitch",
                        "atlassian",
                        "stripe",
                        "slack"
                      ],
                      "description": "Which dedup source matched."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Stored. If the webhook rule's conditions are empty or match the payload, the rule is applied and 'routed' holds its NAME; if the conditions do not match, the submission is still stored and the response is 201 with routed: [] and records: 0. A swallowed routing failure looks the same, including an endpoint rule whose conditions/mappings JSON fails to parse (#304): the submission is marked forward_status 'failed' with reason 'Endpoint rule is malformed', listed on the project's Errors tab as routing_failed, and routed by the outbox sweep once the rule is fixed. When the conditions did not match, the response also carries filtered: true and the submission is marked forward_status 'filtered' (#287); a swallowed routing failure does not.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "routed",
                    "records"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "routed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "the applied rule's name, or empty when the conditions did not match"
                    },
                    "records": {
                      "type": "integer",
                      "description": "routed.length"
                    },
                    "filtered": {
                      "type": "boolean",
                      "enum": [
                        true
                      ],
                      "description": "Present, and true, only when the endpoint's own criteria rejected the payload (#287). The submission is kept (and counted against the quota) with forward_status 'filtered' and no record, and is listed on the project's Errors tab: GET /admin/api/projects/{project_id}/errors."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body could not be parsed: 'Body must be valid JSON' (the body declares application/json, a +json type or no Content-Type, and does not parse) or 'Body is not valid multipart/form-data'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Signature-required keys: 'Signature required' or 'Invalid signature' (X-Hookie-Signature). Endpoints with verification (#210): the provider's signature is missing, forged or stale; reason missing_header, bad_signature or stale_timestamp, with the scheme. Nothing is stored or counted. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "error: 'Source IP not allowed' \u2014 the client IP failed the key allowlist or the tenant allowlist; the block is audited. Also `reason: \"workspace_suspended\"` when the workspace has been suspended by the operator: nothing is stored, counted or charged, so a sender should retry after the workspace is reinstated. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Two messages. 'Unknown ingest key' \u2014 unknown, revoked or expired key, hash mismatch or missing tenant row. 'Unknown endpoint' \u2014 no endpoint with that slug in the key's PROJECT. The lookup is confined to the key's project, so a slug that belongs to another project of the same workspace is 404 too. The slug is resolved BEFORE quota is charged and before the submission is stored, so a bad slug costs nothing and leaves no pending row.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "error: 'Body exceeds 1000000 bytes'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The body is not JSON and its Content-Type is neither a form encoding nor a textual type that can be stored as {body, content_type} (for example application/octet-stream or image/png). The error names the Content-Type. Nothing is stored and no quota is charged. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Two cases, each with a Retry-After response header in seconds that equals the retry_after body field. Edge burst limit: about 100 requests per 10 seconds per credential (one endpoint URL, or one ingest key), the same on every plan: {error: 'Rate limit exceeded', retry_after: 10} with Retry-After: 10. It is a best-effort burst guard, not an exact count: it is counted separately at each Cloudflare location and catches up within seconds, so a short burst can get through above it. Monthly plan quota, the exact count (the plan's events plus its soft cap: +10% on Pro and Team, none on Free): {error: 'Monthly event quota reached: this event was not stored. The quota resets at <reset_at>; upgrade the plan to raise it sooner.', reason: 'quota_exceeded', limit: <events at which ingest refuses>, reset_at: '<00:00 UTC on the 1st of next month>', upgrade_url: '<APP_ORIGIN>/#/settings/billing', retry_after: 3600} with Retry-After: 3600. The event is refused, not queued: nothing is stored, so a sender must retry it after reset_at or an upgrade. Only the quota case carries reason, limit, reset_at and upgrade_url; branch on reason, not on the error text. Both limiters fail open if their backing service is unavailable. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer"
                        },
                        "upgrade_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "quota_exceeded"
                          ],
                          "description": "monthly-quota 429 only: quota_exceeded"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "monthly-quota 429 only: the event count at which ingest refuses (the plan's events plus its soft cap)"
                        },
                        "reset_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "monthly-quota 429 only: when the quota resets, 00:00 UTC on the 1st of next month"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 10 for the edge burst limit, 3600 for the monthly quota. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "error: 'Key misconfigured' (signing secret could not be decrypted) or 'Could not store submission'. A rule whose conditions/mappings JSON fails to parse is no longer a 500 (#304): the event has already been stored and counted by then, so it is answered 201 as a routing failure (see 201).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The endpoint exists but is disabled: {error: 'This endpoint is disabled, so events sent to it are not being stored. ...', reason: 'endpoint_disabled'}. Nothing is stored and no quota is charged. It is deliberately not the 404 of a wrong URL: providers treat a 404 as a dead endpoint (and may disable it or drop the events), while a 503 is retried, so events sent while it is disabled can still arrive if it is enabled again within the provider's retry window. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "reason": {
                          "type": "string",
                          "enum": [
                            "endpoint_disabled"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "508": {
            "description": "Loop detected: the request's `Hookie-Hop` header says this event has already passed through Hookie 8 times, which almost always means a destination is delivering back into an endpoint that feeds it. Nothing is stored. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Key-authenticated form of an endpoint's URL. PUT is accepted identically (ingestWebhookEventPut); any other method returns 405 {\"error\":\"Use POST\"}. Every stored submission keeps the request's method, a filtered header map, the query string, the Content-Type and the source IP (ING-3, #193), shown on the record (GET .../records/{id}) and on correlate as `request`. Credentials are never stored: Authorization, Proxy-Authorization, Cookie and any header or query parameter named like a credential (key, api-key, token, secret, password, session, auth) is stored as \"[redacted]\"; Cloudflare and proxy hop headers (cf-*, x-forwarded-*, x-real-ip, true-client-ip, cdn-loop, host) are dropped.",
        "parameters": [
          {
            "name": "ingest_key",
            "in": "path",
            "required": true,
            "description": "The plaintext ingest key (looked up by its full SHA-256 hash, #177; revoked and expired keys never match). It authenticates the request; the tenant it belongs to scopes the webhook lookup.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Webhook slug within the key's project; a slug of another project, even in the same workspace, is 404. A bare slug selects the most recently created enabled webhook with that base slug; a versioned slug of the form '<base>-v<version>' (version = 1+ digits, optionally .digits, e.g. 'orders-v2' or 'orders-v2.1') pins that exact enabled version. When the slug names only a disabled webhook the answer is 503.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional. A repeat key short-circuits with 200 \u2014 after the webhook slug is resolved, but before the monthly quota is charged.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Hookie-Signature",
            "in": "header",
            "required": false,
            "description": "Required only when the ingest key has require_signature set (this route uses the KEY's signature setting and signing secret, not the webhook's). Format 't=<unix-timestamp>,v1=<hex>', v1 = HMAC-SHA-256 over '<t>.<raw body>'.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Form encodings are flattened (repeats become arrays, file parts become {filename, type, size} with the bytes discarded); every other body is parsed as JSON first, and textual non-JSON bodies are stored as {body, content_type} (see ingestEvent), which the rule's conditions and mappings address as `body` and `content_type`; an empty body becomes {}. The parsed payload is evaluated against the webhook's rule conditions and reshaped by its mappings (empty conditions match everything, empty mappings are identity). Over 1,000,000 bytes yields 413.",
          "content": {
            "application/json": {
              "schema": {
                "description": "Any JSON value; its shape is whatever the webhook's rule conditions and mappings expect."
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ]
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "filename",
                        "type",
                        "size"
                      ],
                      "properties": {
                        "filename": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "size": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "Plain text, e.g. an alert. Stored as {body, content_type} unless it parses as JSON."
              }
            },
            "application/xml": {
              "schema": {
                "type": "string",
                "description": "XML, e.g. a SOAP outbound message (text/xml and +xml types too). Stored as {body, content_type}."
              }
            },
            "text/csv": {
              "schema": {
                "type": "string",
                "description": "CSV. Stored whole as {body, content_type}; rows are not split."
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "NDJSON / JSON Lines. Stored whole as {body, content_type}; lines are not split into events. A single line that is itself valid JSON is stored as JSON."
              }
            }
          }
        },
        "security": [
          {
            "ingestKey": []
          }
        ]
      },
      "put": {
        "operationId": "ingestWebhookEventPut",
        "summary": "Ingest into a configured endpoint (PUT alias of ingestWebhookEvent)",
        "tags": [
          "Ingest"
        ],
        "responses": {
          "200": {
            "description": "Duplicate - this endpoint (or, on /v1/ingest, this ingest key) already stored a submission with the same dedup key, so the retry is answered with the first submission's id and nothing is stored or counted (ING-9, #193). The dedup key is, in order: an Idempotency-Key header; else a provider delivery id kept the same across that provider's retries - webhook-id (Standard Webhooks), svix-id, X-GitHub-Delivery, X-Shopify-Webhook-Id, X-Gitlab-Event-UUID, I-Twilio-Idempotency-Token, Linear-Delivery, Twitch-Eventsub-Message-Id, X-Atlassian-Webhook-Identifier; else Stripe's event id (a body with object 'event' and an evt_ id) or Slack's event_id (type 'event_callback'). The key space is the ENDPOINT's, not the workspace's: two endpoints may receive the same key. `deduplicated_by` names which source matched. A request that loses the unique-index race has already been counted against the monthly quota.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "idempotent"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "idempotent": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "deduplicated_by": {
                      "type": "string",
                      "enum": [
                        "idempotency-key",
                        "standard-webhooks",
                        "svix",
                        "github",
                        "shopify",
                        "gitlab",
                        "twilio",
                        "linear",
                        "twitch",
                        "atlassian",
                        "stripe",
                        "slack"
                      ],
                      "description": "Which dedup source matched."
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Stored; 'routed' holds the webhook rule's name, or is empty when its conditions did not match. When the conditions did not match, the response also carries filtered: true and the submission is marked forward_status 'filtered' (#287); a swallowed routing failure does not. An endpoint rule whose JSON fails to parse is a routing failure (#304): 201 with routed: [], the submission marked 'failed' with reason 'Endpoint rule is malformed', and routed by the outbox sweep once the rule is fixed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submission_id",
                    "routed",
                    "records"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "submission_id": {
                      "type": "string"
                    },
                    "routed": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "records": {
                      "type": "integer"
                    },
                    "filtered": {
                      "type": "boolean",
                      "enum": [
                        true
                      ],
                      "description": "Present, and true, only when the endpoint's own criteria rejected the payload (#287). The submission is kept (and counted against the quota) with forward_status 'filtered' and no record, and is listed on the project's Errors tab: GET /admin/api/projects/{project_id}/errors."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body could not be parsed: 'Body must be valid JSON' (the body declares application/json, a +json type or no Content-Type, and does not parse) or 'Body is not valid multipart/form-data'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Signature-required keys: 'Signature required' or 'Invalid signature' (X-Hookie-Signature). Endpoints with verification (#210): the provider's signature is missing, forged or stale; reason missing_header, bad_signature or stale_timestamp, with the scheme. Nothing is stored or counted. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "error: 'Source IP not allowed'. Also `reason: \"workspace_suspended\"` when the workspace has been suspended by the operator: nothing is stored, counted or charged, so a sender should retry after the workspace is reinstated. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "error: 'Unknown ingest key' or 'Unknown endpoint' (no endpoint with that slug in the key's project).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "error: 'Body exceeds 1000000 bytes'. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The body is not JSON and its Content-Type is neither a form encoding nor a textual type that can be stored as {body, content_type} (for example application/octet-stream or image/png). The error names the Content-Type. Nothing is stored and no quota is charged. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Two cases, each with a Retry-After response header in seconds that equals the retry_after body field. Edge burst limit: about 100 requests per 10 seconds per credential (one endpoint URL, or one ingest key), the same on every plan: {error: 'Rate limit exceeded', retry_after: 10} with Retry-After: 10. It is a best-effort burst guard, not an exact count: it is counted separately at each Cloudflare location and catches up within seconds, so a short burst can get through above it. Monthly plan quota, the exact count (the plan's events plus its soft cap: +10% on Pro and Team, none on Free): {error: 'Monthly event quota reached: this event was not stored. The quota resets at <reset_at>; upgrade the plan to raise it sooner.', reason: 'quota_exceeded', limit: <events at which ingest refuses>, reset_at: '<00:00 UTC on the 1st of next month>', upgrade_url: '<APP_ORIGIN>/#/settings/billing', retry_after: 3600} with Retry-After: 3600. The event is refused, not queued: nothing is stored, so a sender must retry it after reset_at or an upgrade. Only the quota case carries reason, limit, reset_at and upgrade_url; branch on reason, not on the error text. Both limiters fail open if their backing service is unavailable. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer"
                        },
                        "upgrade_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "quota_exceeded"
                          ],
                          "description": "monthly-quota 429 only: quota_exceeded"
                        },
                        "limit": {
                          "type": "integer",
                          "description": "monthly-quota 429 only: the event count at which ingest refuses (the plan's events plus its soft cap)"
                        },
                        "reset_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "monthly-quota 429 only: when the quota resets, 00:00 UTC on the 1st of next month"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 10 for the edge burst limit, 3600 for the monthly quota. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "error: 'Key misconfigured' or 'Could not store submission'. A malformed endpoint rule is answered 201 as a routing failure, not 500 (#304).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The endpoint exists but is disabled: {error: 'This endpoint is disabled, so events sent to it are not being stored. ...', reason: 'endpoint_disabled'}. Nothing is stored and no quota is charged. It is deliberately not the 404 of a wrong URL: providers treat a 404 as a dead endpoint (and may disable it or drop the events), while a 503 is retried, so events sent while it is disabled can still arrive if it is enabled again within the provider's retry window. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "reason": {
                          "type": "string",
                          "enum": [
                            "endpoint_disabled"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "508": {
            "description": "Loop detected: the request's `Hookie-Hop` header says this event has already passed through Hookie 8 times, which almost always means a destination is delivering back into an endpoint that feeds it. Nothing is stored. Logged to the endpoint's rejection log (OBS-3, #193): see GET /admin/api/projects/{project_id}/webhooks/{id}/activity. A refusal never counts against the event quota.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "PUT alias; behaviour is identical to the POST operation. Every stored submission keeps the request's method, a filtered header map, the query string, the Content-Type and the source IP (ING-3, #193), shown on the record (GET .../records/{id}) and on correlate as `request`. Credentials are never stored: Authorization, Proxy-Authorization, Cookie and any header or query parameter named like a credential (key, api-key, token, secret, password, session, auth) is stored as \"[redacted]\"; Cloudflare and proxy hop headers (cf-*, x-forwarded-*, x-real-ip, true-client-ip, cdn-loop, host) are dropped.",
        "parameters": [
          {
            "name": "ingest_key",
            "in": "path",
            "required": true,
            "description": "The plaintext ingest key.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Webhook slug in the key's project; '<base>-v<version>' pins a version, a bare slug takes the newest enabled one.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional; a repeat key short-circuits with 200 before quota is charged.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Hookie-Signature",
            "in": "header",
            "required": false,
            "description": "Required only when the ingest key has require_signature. Format 't=<unix-timestamp>,v1=<hex>'.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "Identical to the POST form: form encodings flattened, everything else parsed as JSON first with textual non-JSON bodies stored as {body, content_type}, empty body becomes {}, 1,000,000-byte cap.",
          "content": {
            "application/json": {
              "schema": {
                "description": "Any JSON value."
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  ]
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "additionalProperties": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "filename",
                        "type",
                        "size"
                      ],
                      "properties": {
                        "filename": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "size": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            },
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "Plain text, e.g. an alert. Stored as {body, content_type} unless it parses as JSON."
              }
            },
            "application/xml": {
              "schema": {
                "type": "string",
                "description": "XML, e.g. a SOAP outbound message (text/xml and +xml types too). Stored as {body, content_type}."
              }
            },
            "text/csv": {
              "schema": {
                "type": "string",
                "description": "CSV. Stored whole as {body, content_type}; rows are not split."
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "NDJSON / JSON Lines. Stored whole as {body, content_type}; lines are not split into events. A single line that is itself valid JSON is stored as JSON."
              }
            }
          }
        },
        "security": [
          {
            "ingestKey": []
          }
        ]
      }
    },
    "/v1/stream": {
      "get": {
        "operationId": "streamEvents",
        "summary": "Tail the ingest key's project live event stream over SSE or WebSocket",
        "tags": [
          "Streaming"
        ],
        "responses": {
          "101": {
            "description": "WebSocket upgrade accepted (the request carried 'Upgrade: websocket'). Each live event arrives as one JSON text frame: {seq, id, dataset, received_at, data, project_id, webhook_id}. When a resume cursor was supplied and part of the requested window had been evicted, a {\"type\":\"stream-gap\",\"missed_from\":<int>,\"resume_from\":<int>} frame is sent first, followed by the replayable buffered events. The socket is hibernatable and answers a 'ping' text frame with 'pong'. The live path is lossy by contract \u2014 durable delivery is the outbound-delivery queue, not this stream."
          },
          "200": {
            "description": "Server-sent event stream (no Upgrade header). Frames: ': connected' on open, ': keep-alive' every 25 seconds, one 'id: <seq>\\ndata: <json>\\n\\n' frame per event where the JSON is {seq, id, dataset, received_at, data, project_id, webhook_id}, and \u2014 on a resume that missed evicted events \u2014 an un-ided 'event: stream-gap\\ndata: {\"missed_from\":<int>,\"resume_from\":<int>}' control frame. A subscriber that stops draining is dropped once its backlog runs past the stream's high-water mark.",
            "headers": {
              "X-Accel-Buffering": {
                "description": "Always 'no' \u2014 disables proxy/CDN buffering so events flush immediately.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "no"
                  ]
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "Always '*'. Safe because the stream is key-authenticated and never cookie-authenticated; it also blocks any credentialed cross-origin EventSource.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "*"
                  ]
                }
              },
              "Cache-Control": {
                "description": "Always 'no-store'.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "SSE frame stream; Content-Type is 'text/event-stream; charset=utf-8'."
                }
              }
            }
          },
          "401": {
            "description": "error: 'Ingest key required' \u2014 neither an 'Authorization: Bearer <key>' header nor a ?key= query parameter was supplied (or both were empty).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The request named a project_id other than the key's own, the key has no project (a pre-projects key; create a new one), or the source IP is outside the key's or workspace's allowlist. Also `reason: \"workspace_suspended\"` when the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "error: 'Unknown ingest key' \u2014 no live key with this prefix (revoked, or past its expires_at), hash mismatch, or the key's tenant row is missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The tenant's plan cap on concurrent stream connections is reached (open SSE responses and WebSockets are counted together). This response comes from the stream Durable Object rather than the shared apiError helper, so it carries a Retry-After header and no CSP/security headers.",
            "headers": {
              "Retry-After": {
                "description": "Always '30'.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "30"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "required": [
                        "limit"
                      ],
                      "properties": {
                        "limit": {
                          "type": "integer",
                          "description": "the plan's concurrent-connection cap"
                        }
                      }
                    }
                  ],
                  "description": "error is always 'stream connection limit reached'."
                }
              }
            }
          }
        },
        "description": "Read-only live tail of every event fanned out in the key's PROJECT. It never exposes stored records or the admin API. An ingest key belongs to one project, and the stream it opens is pinned to that project: it cannot be widened by leaving project_id off, and naming a different project is a 403. Narrow further with dataset. GET only: any other method returns 405 {\"error\":\"Use GET\"}. If the workspace is suspended while a connection is open, the connection is closed: a WebSocket first receives {\"type\":\"workspace-suspended\",\"reason\":\"workspace_suspended\",\"error\":\"...\"} and then closes with code 1008 and reason `workspace_suspended`; an SSE stream receives a `workspace-suspended` event carrying the same object and ends. Reconnecting is refused with 403 until the workspace is reinstated.",
        "parameters": [
          {
            "name": "key",
            "in": "query",
            "required": false,
            "description": "Ingest key as a query parameter, for browser EventSource clients that cannot set headers. Used only when no 'Authorization: Bearer <ingest key>' header is present; if neither is supplied the request is 401. Same key resolution as ingest (looked up by its full SHA-256 hash, #177; revoked and expired keys never match).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "after_seq",
            "in": "query",
            "required": false,
            "description": "Resume cursor: replay buffered events with seq greater than this value. Ignored when a Last-Event-ID header is present. Non-numeric or negative values are treated as absent. The resume buffer holds at most 1000 events and drops anything older than 5 minutes; a request for evicted events still succeeds but is preceded by a stream-gap control event.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "Last-Event-ID",
            "in": "header",
            "required": false,
            "description": "Standard SSE resume header, sent automatically by EventSource on reconnect. Takes precedence over after_seq when present. Same semantics and same 1000-event / 5-minute buffer window.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "Upgrade",
            "in": "header",
            "required": false,
            "description": "Send exactly 'websocket' (compared case-sensitively) to get a 101 WebSocket upgrade instead of an SSE stream. Any other value, or its absence, yields the SSE response.",
            "schema": {
              "type": "string",
              "enum": [
                "websocket"
              ]
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "description": "Optional, and only ever the key's own project: the stream is already pinned to it, so this can only restate it. Naming a different project returns 403 {\"error\":\"An ingest key can only stream its own project.\"}. STRICT: an event with no project (a workflow instance outside one) never reaches a key's stream.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dataset",
            "in": "query",
            "required": false,
            "description": "Only deliver events in this dataset. Combines with project_id \u2014 both have to match. Applied server-side for the same reason.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "ingestKey": []
          }
        ]
      }
    },
    "/portal/api/me": {
      "get": {
        "operationId": "getPortalMe",
        "summary": "Get the portal context for the caller's token or session",
        "tags": [
          "Customer Portal"
        ],
        "responses": {
          "200": {
            "description": "Portal identity and the datasets it exposes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "portal",
                    "customer_id",
                    "event_types"
                  ],
                  "properties": {
                    "portal": {
                      "type": "object",
                      "required": [
                        "name",
                        "branding"
                      ],
                      "properties": {
                        "name": {
                          "type": "string",
                          "description": "portal_configs.name \u2014 the display name the workspace gave this portal."
                        },
                        "branding": {
                          "type": "object",
                          "description": "The portal's branding, re-validated on the way out (#198): only an https `logo_url` and a `#rrggbb` `primary_color` are ever returned; anything else stored before validation existed is dropped.",
                          "properties": {
                            "logo_url": {
                              "type": "string",
                              "format": "uri"
                            },
                            "primary_color": {
                              "type": "string",
                              "pattern": "^#[0-9a-f]{6}$"
                            }
                          },
                          "additionalProperties": false
                        }
                      }
                    },
                    "customer_id": {
                      "type": "string",
                      "description": "The workspace's own identifier for this end customer, taken from the token (portal_tokens.customer_id). Not settable by the customer. The portal page does not show it; it shows `customer_name`."
                    },
                    "event_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Dataset names this portal exposes; [] if the stored value is unparseable. An empty array means the portal exposes nothing: no destination can be added, and none of its customers' destinations receives anything."
                    },
                    "customer_name": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The display name the vendor gave this customer when issuing the token (#198); null when none was given, and the portal then names nobody."
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time",
                      "description": "When the token expires; null for never."
                    }
                  }
                },
                "example": {
                  "portal": {
                    "name": "Acme Webhooks",
                    "branding": {
                      "primary_color": "#1f6feb",
                      "logo_url": "https://cdn.example.com/logo.png"
                    }
                  },
                  "customer_id": "cus_9182",
                  "event_types": [
                    "orders",
                    "invoices"
                  ],
                  "customer_name": "Globex Corp",
                  "expires_at": null
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled. Body: {\"error\":\"Invalid or expired portal token\"}.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/portal/api/event-types": {
      "get": {
        "operationId": "listPortalEventTypes",
        "summary": "List the dataset/event types this portal exposes",
        "tags": [
          "Customer Portal"
        ],
        "responses": {
          "200": {
            "description": "The portal's exposed dataset names (same array as `event_types` on /portal/api/me).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "event_types"
                  ],
                  "properties": {
                    "event_types": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": {
                  "event_types": [
                    "orders",
                    "invoices"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/portal/api/destinations": {
      "get": {
        "operationId": "listPortalDestinations",
        "summary": "List this customer's own destinations",
        "tags": [
          "Customer Portal"
        ],
        "responses": {
          "200": {
            "description": "Destinations owned by this token's (tenant, project, portal, customer), newest first. No pagination and no limit. The signing secret is not in this list; GET /portal/api/destinations/{id}/secret reveals it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "destinations"
                  ],
                  "properties": {
                    "destinations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "url",
                          "dataset_filter",
                          "enabled",
                          "created_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Destination id (UUID)."
                          },
                          "name": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "dataset_filter": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Raw stored column: a JSON-encoded array of dataset names (e.g. \"[\\\"orders\\\"]\"), returned verbatim, NOT parsed into an array. It can only narrow what arrives: every delivery is also checked against the portal's CURRENT event_types, so a dataset the portal stops exposing stops arriving, and a portal that is switched off delivers nothing."
                          },
                          "enabled": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ],
                            "description": "SQLite integer flag, not a JSON boolean."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "previous_secret_expires_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "While a rotated-out signing secret still signs (#198, #175): when it stops. Null outside the overlap window. The secret itself is never returned here."
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "destinations": [
                    {
                      "id": "3f2b\u2026",
                      "name": "Prod receiver",
                      "url": "https://hooks.acme.test/in",
                      "dataset_filter": "[\"orders\"]",
                      "enabled": 1,
                      "created_at": "2026-09-01T12:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      },
      "post": {
        "operationId": "createPortalDestination",
        "summary": "Create a destination for this customer",
        "tags": [
          "Customer Portal"
        ],
        "responses": {
          "201": {
            "description": "Created. The signing secret is returned once and never again \u2014 the stored copy is encrypted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "signing_secret"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "New destination id (UUID)."
                    },
                    "signing_secret": {
                      "type": "string",
                      "description": "HMAC signing secret, \"whsec_\" followed by 48 lowercase hex characters. Shown once."
                    }
                  }
                },
                "example": {
                  "id": "9c1f\u2026",
                  "signing_secret": "whsec_0a1b\u2026"
                }
              }
            }
          },
          "400": {
            "description": "Validation failure: the body is not a JSON object, the name or url fails the same validation the console applies to a destination (name 1\u201380 characters, an https URL that parses), or \"dataset(s) not exposed by this portal: <comma-separated names>\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled. Also answered when the token was revoked or expired while the request was in flight (PORT-2g, #321): the write re-checks the token inside the statement, so an endpoint is never added or switched on for a customer the vendor has just offboarded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Missing X-Requested-With header` \u2014 a write made with the session cookie must carry `X-Requested-With: fetch`. Not required with a bearer token. Also `reason: \"workspace_suspended\"` when the workspace is suspended: the portal is read-only until it is reinstated, and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The portal exposes no event types, so a destination would receive nothing. Body: {\"error\":\"This portal does not share any event types yet, so a destination would receive nothing. Contact the service you are connecting to.\"}.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The workspace is at its plan's destination limit. The message is written for the vendor's customer, who cannot upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "description": "Only `name`, `url` and `dataset_filter` are customer-settable. Every other column is server-controlled and cannot be influenced by the request: `secret` (generated, whsec_ + 48 hex, AES-GCM encrypted at rest), `enabled` (always 1 on create), `created_by` (always \"portal:<customer_id>\"), `tenant_id`, `project_id`, `portal_id`, `customer_id` (all taken from the token), `event_filter` (never set) and `created_at`. Unknown properties in the body are ignored, not rejected.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "url"
                ],
                "additionalProperties": true,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Must be a non-empty string after trimming, at most 80 characters; stored trimmed."
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "pattern": "^[Hh][Tt][Tt][Pp][Ss]://",
                    "description": "Must be an https URL that parses. Validated exactly as the console validates a destination URL. A Slack incoming-webhook URL (hooks.slack.com) is refused (400): Slack accepts only its own message format, and the URL is a credential (#313)."
                  },
                  "dataset_filter": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional. Event types to receive, a subset of the portal's event_types; omitted or empty means all of them."
                  }
                }
              },
              "example": {
                "name": "Prod receiver",
                "url": "https://hooks.acme.test/in",
                "dataset_filter": [
                  "orders"
                ]
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/portal/api/destinations/{id}": {
      "put": {
        "operationId": "updatePortalDestination",
        "summary": "Edit, pause or resume one of this customer's destinations",
        "tags": [
          "Customer Portal"
        ],
        "responses": {
          "200": {
            "description": "Updated (also returned when the values were unchanged but the row matched).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled. Also answered when the token was revoked or expired while the request was in flight (PORT-2g, #321): the write re-checks the token inside the statement, so `enabled: true` never switches an endpoint on for a customer the vendor has just offboarded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No destination matched this id for the token's tenant, project, portal and customer \u2014 including one the customer deleted. Body: {\"error\":\"Destination not found\"}.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Missing X-Requested-With header` \u2014 a write made with the session cookie must carry `X-Requested-With: fetch`. Not required with a bearer token. Also `reason: \"workspace_suspended\"` when the workspace is suspended: the portal is read-only until it is reinstated, and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "`Nothing to update: send name, url, dataset_filter and/or enabled`; a name that is empty or over 80 characters; a url that is not a valid https URL; `enabled` that is not a boolean; or a dataset_filter that is empty or names a dataset the portal does not expose.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "enabled: true on a destination paused because the vendor's plan is over its limit (#212). Only the vendor can lift it, by upgrading.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id. The update is scoped to the token's tenant, portal and customer, so another customer's id simply does not match.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Only what is sent changes. Audited as update_destination with the customer (`portal|<customer_id>`) as the actor. Turning a destination back on sends what it held while paused.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https only. A Slack incoming-webhook URL (hooks.slack.com) is refused (400): Slack accepts only its own message format, and the URL is a credential (#313)."
                  },
                  "dataset_filter": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    },
                    "description": "Event types (datasets) this destination receives: a non-empty subset of the portal's event_types. Anything outside them, or an empty list, is a 400."
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      },
      "patch": {
        "operationId": "patchPortalDestination",
        "summary": "Edit, pause or resume one of this customer's destinations",
        "tags": [
          "Customer Portal"
        ],
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled. Also answered when the token was revoked or expired while the request was in flight (PORT-2g, #321): the write re-checks the token inside the statement, so `enabled: true` never switches an endpoint on for a customer the vendor has just offboarded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No destination matched this id for the token's tenant, project, portal and customer \u2014 including one the customer deleted. Body: {\"error\":\"Destination not found\"}.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Missing X-Requested-With header` \u2014 a write made with the session cookie must carry `X-Requested-With: fetch`. Not required with a bearer token. Also `reason: \"workspace_suspended\"` when the workspace is suspended: the portal is read-only until it is reinstated, and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "`Nothing to update: send name, url, dataset_filter and/or enabled`; a name that is empty or over 80 characters; a url that is not a valid https URL; `enabled` that is not a boolean; or a dataset_filter that is empty or names a dataset the portal does not expose.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "enabled: true on a destination paused because the vendor's plan is over its limit (#212). Only the vendor can lift it, by upgrading.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id. Scoped to the token's tenant, portal and customer.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Only what is sent changes. Audited as update_destination with the customer (`portal|<customer_id>`) as the actor. Turning a destination back on sends what it held while paused.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https only. A Slack incoming-webhook URL (hooks.slack.com) is refused (400): Slack accepts only its own message format, and the URL is a credential (#313)."
                  },
                  "dataset_filter": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    },
                    "description": "Event types (datasets) this destination receives: a non-empty subset of the portal's event_types. Anything outside them, or an empty list, is a 400."
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      },
      "delete": {
        "operationId": "deletePortalDestination",
        "summary": "Delete one of this customer's destinations",
        "description": "The console's delete (#198): the row is severed from the project and switched off, since past deliveries still reference it. It disappears from every list, receives nothing, and cannot be switched back on. Audited as delete_destination.",
        "tags": [
          "Customer Portal"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id, scoped to the token's tenant, project, portal and customer: another customer's id answers 404 like one that never existed.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Missing X-Requested-With header` \u2014 a write made with the session cookie must carry `X-Requested-With: fetch`. Not required with a bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No destination matched. Body: {\"error\":\"Destination not found\"}.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/portal/api/deliveries": {
      "get": {
        "operationId": "listPortalDeliveries",
        "summary": "List recent delivery attempts to this customer's destinations",
        "tags": [
          "Customer Portal"
        ],
        "responses": {
          "200": {
            "description": "Up to 200 deliveries, newest first, to destinations owned by this token's portal + customer in the portal's project. There are no query parameters. Each row names its destination and carries the error and the first 300 characters of the endpoint's response; GET /portal/api/deliveries/{id} has the rest.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deliveries"
                  ],
                  "properties": {
                    "deliveries": {
                      "type": "array",
                      "maxItems": 200,
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "dataset",
                          "status",
                          "response_code",
                          "response_ms",
                          "attempts",
                          "created_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Delivery id."
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "queued",
                              "delivering",
                              "delivered",
                              "failed",
                              "dead",
                              "paused",
                              "cancelled"
                            ]
                          },
                          "response_code": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "HTTP status returned by the customer's endpoint; null before a response was seen."
                          },
                          "response_ms": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "attempts": {
                            "type": "integer"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "destination_id": {
                            "type": "string"
                          },
                          "destination_name": {
                            "type": "string"
                          },
                          "max_attempts": {
                            "type": "integer"
                          },
                          "next_retry_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "When the next attempt is due; null when none is scheduled."
                          },
                          "error": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Why the last attempt failed, e.g. `HTTP 500` or a network error."
                          },
                          "response_preview": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "maxLength": 300,
                            "description": "The first 300 characters of what the endpoint answered."
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "deliveries": [
                    {
                      "id": "d81c\u2026",
                      "dataset": "orders",
                      "status": "delivered",
                      "response_code": 200,
                      "response_ms": 142,
                      "attempts": 1,
                      "created_at": "2026-09-10T09:31:02.104Z",
                      "destination_id": "5b1e\u2026",
                      "destination_name": "Orders endpoint",
                      "max_attempts": 8,
                      "next_retry_at": null,
                      "error": null,
                      "response_preview": "ok"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/admin/api/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Current user, workspace and role",
        "tags": [
          "Account"
        ],
        "responses": {
          "200": {
            "description": "The signed-in user, the resolved workspace (tenant) and the caller effective role. workspaceSlug is the first path segment of a public webhook URL. workspaceName is the workspace's display name, set in Settings \u2192 Workspace (PATCH /admin/api/workspace).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "user",
                    "tenantId",
                    "role",
                    "plan",
                    "workspaceSlug",
                    "workspaceName",
                    "platform_admin",
                    "workspaceStatus",
                    "workspaceClosure",
                    "retentionGrace",
                    "signInMethod",
                    "googleWorkspaceDomain",
                    "googleWorkspaceNotice"
                  ],
                  "properties": {
                    "user": {
                      "type": "object",
                      "required": [
                        "id",
                        "email",
                        "name"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string"
                        },
                        "name": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    },
                    "tenantId": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "owner",
                        "admin",
                        "developer",
                        "viewer",
                        "member"
                      ],
                      "description": "For an OAuth-connected agent this is the membership role already clipped to the granted scopes."
                    },
                    "plan": {
                      "type": "string",
                      "enum": [
                        "free",
                        "standard",
                        "team"
                      ]
                    },
                    "workspaceSlug": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "workspaceName": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The workspace's display name."
                    },
                    "platform_admin": {
                      "type": "boolean"
                    },
                    "signInMethod": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "email_code",
                        "github",
                        "google",
                        "sso",
                        "password",
                        "passkey",
                        "other",
                        null
                      ],
                      "description": "How this console session was signed in (SEC-12, #314), from the sealed WorkOS session. null for an OAuth agent or an admin API key, which do not sign in, and when WorkOS did not record a method. It names the first factor only: whether a second factor ran is not recorded in the session."
                    },
                    "workspaceStatus": {
                      "type": "string",
                      "enum": [
                        "active",
                        "suspended"
                      ],
                      "description": "`suspended` while the workspace is suspended: reads work, and writes other than billing answer 403 `workspace_suspended`."
                    },
                    "api_key": {
                      "type": "object",
                      "description": "Present only when the call was made with an admin API key: which key, and the one project it is limited to (null for every project).",
                      "required": [
                        "id",
                        "project_id"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "project_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    },
                    "workspaceClosure": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/WorkspaceClosure"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Set while the owner's closure is pending (the workspace is also `suspended`); null otherwise."
                    },
                    "retentionGrace": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/RetentionGrace"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Set for 7 days after a downgrade that shortened the workspace's retention: until `ends_at` the previous plan's retention still applies, and after it data older than the current plan's retention is deleted. Null otherwise."
                    },
                    "googleWorkspaceDomain": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The Google Workspace domain (Google's `hd`) Google verified for this console session's Google sign-in (#326). null for any other sign-in, a personal Google account, an agent or an API key, and where the WorkOS environment does not return Google's OAuth tokens."
                    },
                    "googleWorkspaceNotice": {
                      "oneOf": [
                        {
                          "type": "object",
                          "required": [
                            "code",
                            "domain"
                          ],
                          "properties": {
                            "code": {
                              "type": "string",
                              "const": "member_limit_reached"
                            },
                            "domain": {
                              "type": "string",
                              "description": "The person's own Google Workspace domain. Nothing about the linked workspace (its name, members or limit) is included: the person is not a member of it."
                            }
                          }
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Set when this Google sign-in's domain is linked to a workspace the person could not join because it is at its plan's member limit (#326); null otherwise."
                    },
                    "agent_account": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "Set when an AI agent registered this workspace itself (#331): which agent, and whether a person has claimed it. Null for every other workspace.",
                      "required": [
                        "id",
                        "name",
                        "claimed",
                        "claimed_at",
                        "created_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "claimed": {
                          "type": "boolean"
                        },
                        "claimed_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "is_agent_account": {
                      "type": "boolean",
                      "enum": [
                        true
                      ],
                      "description": "Present only when the caller is the self-registered agent itself, before a person claims the workspace (#331)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Read-only identity echo. No project scope. Any role can call it.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/dashboard/summary": {
      "get": {
        "operationId": "getDashboardSummary",
        "summary": "Workspace home dashboard rollup",
        "tags": [
          "Dashboard"
        ],
        "responses": {
          "200": {
            "description": "Workspace-wide (not project-scoped) counters plus a per-project row set.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "plan",
                    "quota",
                    "events_this_month",
                    "deliveries_7d",
                    "failed_7d",
                    "error_rate_7d",
                    "agent_runs_7d",
                    "projects",
                    "ai_tokens_this_month",
                    "ai_tokens_quota",
                    "deliveries_this_month",
                    "deliveries_quota",
                    "deliveries_ceiling",
                    "failing_destinations",
                    "plan_name",
                    "delivered_7d",
                    "in_flight_7d",
                    "destinations",
                    "agent_runs_this_month",
                    "agent_runs_quota"
                  ],
                  "properties": {
                    "plan": {
                      "type": "string",
                      "enum": [
                        "free",
                        "standard",
                        "team"
                      ]
                    },
                    "quota": {
                      "type": "integer",
                      "description": "Plan events-per-month allowance."
                    },
                    "events_this_month": {
                      "type": "integer"
                    },
                    "deliveries_7d": {
                      "type": "integer"
                    },
                    "failed_7d": {
                      "type": "integer"
                    },
                    "error_rate_7d": {
                      "type": "number",
                      "description": "failed_7d / (delivered_7d + failed_7d): a share of FINISHED deliveries, or 0 when none has finished. In-flight deliveries are in neither."
                    },
                    "agent_runs_7d": {
                      "type": "integer"
                    },
                    "projects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "slug",
                          "type",
                          "webhooks",
                          "events_7d",
                          "error_rate_7d",
                          "last_activity",
                          "created_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "webhook",
                              "ai_trigger"
                            ]
                          },
                          "webhooks": {
                            "type": "integer"
                          },
                          "events_7d": {
                            "type": "integer"
                          },
                          "error_rate_7d": {
                            "type": "number",
                            "description": "Failed as a share of the project's finished deliveries over 7 days."
                          },
                          "last_activity": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "ai_tokens_this_month": {
                      "type": "integer",
                      "description": "AI tokens (input + output) used this billing month, across every AI call in the workspace."
                    },
                    "ai_tokens_quota": {
                      "type": "integer",
                      "description": "The plan's monthly AI token allowance. A call made once it is used is refused and the step fails."
                    },
                    "agent_runs_this_month": {
                      "type": "integer",
                      "description": "AI trigger runs counted against this billing month's limit: the workspace's running and done runs created since the 1st (UTC). The same count a run is refused on, so a failed or still-queued run is not in it (#260)."
                    },
                    "agent_runs_quota": {
                      "type": "integer",
                      "description": "The plan's monthly AI trigger run limit (agentRunsPerMonth). No soft cap: once agent_runs_this_month reaches it, each further AI trigger run fails until the month resets. Events are still stored and delivered."
                    },
                    "deliveries_this_month": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Deliveries charged against this month's allowance (#159); null if the usage counter could not be read."
                    },
                    "deliveries_quota": {
                      "type": "integer",
                      "description": "Plan deliveries-per-month allowance."
                    },
                    "deliveries_ceiling": {
                      "type": "integer",
                      "description": "Allowance plus soft overage; past it deliveries are held."
                    },
                    "failing_destinations": {
                      "type": "array",
                      "maxItems": 20,
                      "description": "OBS-2: the workspace's destinations that are failing (a delivery went dead and none has arrived since) or were switched off automatically, switched-off first. Home shows a notice for each, with a one-click re-enable.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "url",
                          "project_id",
                          "project_name",
                          "enabled",
                          "consecutive_dead",
                          "failing_since",
                          "last_failure",
                          "auto_disabled_at",
                          "disabled_reason"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "project_id": {
                            "type": "string"
                          },
                          "project_name": {
                            "type": "string"
                          },
                          "enabled": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ]
                          },
                          "consecutive_dead": {
                            "type": "integer"
                          },
                          "failing_since": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "last_failure": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "auto_disabled_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "disabled_reason": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "plan_name": {
                      "type": "string",
                      "description": "The plan's display name (Free, Pro, Team) - what users see. plan is the internal id."
                    },
                    "delivered_7d": {
                      "type": "integer"
                    },
                    "in_flight_7d": {
                      "type": "integer",
                      "description": "Deliveries in the last 7 days still queued, retrying or held by a paused destination - no outcome yet."
                    },
                    "destinations": {
                      "type": "integer",
                      "description": "Destinations in the workspace (deleted ones excluded)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Aggregated on read from records / deliveries / agent_runs / usage_rollups. Readable by any role.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/dashboard/timeseries": {
      "get": {
        "operationId": "getDashboardTimeseries",
        "summary": "Bucketed activity series for the workspace",
        "tags": [
          "Dashboard"
        ],
        "responses": {
          "200": {
            "description": "Equal-width buckets ending now. 24h and 7d are hourly (24 and 168 buckets); 30d is daily (30 buckets). At most 50000 raw timestamps are scanned per request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "window",
                    "metric",
                    "since",
                    "until",
                    "bucket_ms",
                    "values"
                  ],
                  "properties": {
                    "window": {
                      "type": "string",
                      "enum": [
                        "24h",
                        "7d",
                        "30d"
                      ]
                    },
                    "metric": {
                      "type": "string",
                      "enum": [
                        "events",
                        "deliveries",
                        "agent_runs"
                      ]
                    },
                    "since": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "until": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "bucket_ms": {
                      "type": "integer"
                    },
                    "values": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "bad window or bad metric.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Workspace-wide, not project-scoped. Readable by any role. Counted in SQL, one row per bucket: there is no cap on how many events a window may hold.",
        "parameters": [
          {
            "name": "window",
            "in": "query",
            "required": false,
            "description": "Time window. Defaults to 7d.",
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "30d"
              ],
              "default": "7d"
            }
          },
          {
            "name": "metric",
            "in": "query",
            "required": false,
            "description": "Which table to count. Defaults to events.",
            "schema": {
              "type": "string",
              "enum": [
                "events",
                "deliveries",
                "agent_runs"
              ],
              "default": "events"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/stream": {
      "get": {
        "operationId": "adminStreamEvents",
        "summary": "Tail the current workspace's live event stream over SSE or WebSocket",
        "tags": [
          "Streaming"
        ],
        "responses": {
          "101": {
            "description": "WebSocket upgrade accepted (the request carried 'Upgrade: websocket'). Each live event arrives as one JSON text frame: {seq, id, dataset, received_at, data, project_id, webhook_id}. When a resume cursor was supplied and part of the requested window had been evicted, a {\"type\":\"stream-gap\",\"missed_from\":<int>,\"resume_from\":<int>} frame is sent first, followed by the replayable buffered events. The socket is hibernatable and answers a 'ping' text frame with 'pong'. The live path is lossy by contract \u2014 durable delivery is the outbound-delivery queue, not this stream."
          },
          "200": {
            "description": "Server-sent event stream (no Upgrade header). Frames: ': connected' on open, ': keep-alive' every 25 seconds, one 'id: <seq>\\ndata: <json>\\n\\n' frame per event where the JSON is {seq, id, dataset, received_at, data, project_id, webhook_id}, and \u2014 on a resume that missed evicted events \u2014 an un-ided 'event: stream-gap\\ndata: {\"missed_from\":<int>,\"resume_from\":<int>}' control frame. A subscriber that stops draining is dropped once its backlog runs past the stream's high-water mark.",
            "headers": {
              "X-Accel-Buffering": {
                "description": "Always 'no' \u2014 disables proxy/CDN buffering so events flush immediately.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "no"
                  ]
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "Always '*'. Safe because the stream is key-authenticated and never cookie-authenticated; it also blocks any credentialed cross-origin EventSource.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "*"
                  ]
                }
              },
              "Cache-Control": {
                "description": "Always 'no-store'.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "SSE frame stream; Content-Type is 'text/event-stream; charset=utf-8'."
                }
              }
            }
          },
          "429": {
            "description": "The tenant's plan cap on concurrent stream connections is reached (open SSE responses and WebSockets are counted together). This response comes from the stream Durable Object rather than the shared apiError helper, so it carries a Retry-After header and no CSP/security headers.",
            "headers": {
              "Retry-After": {
                "description": "Always '30'.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "30"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "required": [
                        "limit"
                      ],
                      "properties": {
                        "limit": {
                          "type": "integer",
                          "description": "the plan's concurrent-connection cap"
                        }
                      }
                    }
                  ],
                  "description": "error is always 'stream connection limit reached'."
                }
              }
            }
          },
          "401": {
            "description": "No session cookie and no usable bearer token, or the agent token failed verification against the issuer's JWKS.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "An agent token whose granted scopes do not include hookie:read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "The same live tail as /v1/stream, for the console's Live mode on Deliveries and Observability (it passes project_id) and a connected agent's tooling (the stdio MCP server's tail_stream) rather than for a server-side integration: it authenticates with the browser session or a connected agent's bearer token instead of an ingest key, and the tenant comes from that credential rather than from the key. GET only \u2014 the route resolves before the admin CSRF guard, which only inspects mutating methods.\n\nThe stream is BEST-EFFORT by contract. Durable delivery is the outbound-delivery queue; a tail is for watching, and the absence of an event here never proves the event was not received. The resume buffer holds 1000 events and 5 minutes, and a resume outside that window succeeds but is preceded by a stream-gap control event.\n\nAn agent token needs hookie:read. Its 429 is the plan's concurrent-connection cap, counted across SSE responses and WebSockets together and across every client alike \u2014 a console tab left in Live mode counts as one, and falls back to refreshing every 10 s when refused.",
        "parameters": [
          {
            "name": "after_seq",
            "in": "query",
            "required": false,
            "description": "Resume cursor: replay buffered events with seq greater than this value. Ignored when a Last-Event-ID header is present. Non-numeric or negative values are treated as absent. The resume buffer holds at most 1000 events and drops anything older than 5 minutes; a request for evicted events still succeeds but is preceded by a stream-gap control event.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "Last-Event-ID",
            "in": "header",
            "required": false,
            "description": "Standard SSE resume header, sent automatically by EventSource on reconnect. Takes precedence over after_seq when present. Same semantics and same 1000-event / 5-minute buffer window.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "Upgrade",
            "in": "header",
            "required": false,
            "description": "Send exactly 'websocket' (compared case-sensitively) to get a 101 WebSocket upgrade instead of an SSE stream. Any other value, or its absence, yields the SSE response.",
            "schema": {
              "type": "string",
              "enum": [
                "websocket"
              ]
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "description": "Only deliver events belonging to this project. Applied by the stream Durable Object, not by the client, because the plan caps CONCURRENT CONNECTIONS (one, on Free) \u2014 a subscriber that attached to everything and filtered locally would spend its whole allowance to watch one project. STRICT: an event with no project (a workflow instance outside one, or anything buffered before events carried a project at all) does NOT match a project filter. The per-tenant `seq` cursor is unaffected, so a filtered stream skips numbers by design; a gap in seq is not a lost event. This is not a security boundary \u2014 the tenant gate is; an id from another tenant simply matches nothing.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dataset",
            "in": "query",
            "required": false,
            "description": "Only deliver events in this dataset. Combines with project_id \u2014 both have to match. Applied server-side for the same reason.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects": {
      "get": {
        "operationId": "listProjects",
        "summary": "List projects in the workspace",
        "tags": [
          "Projects"
        ],
        "responses": {
          "200": {
            "description": "Every project of the caller workspace, oldest first, each with its webhook count.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "projects"
                  ],
                  "properties": {
                    "projects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "slug",
                          "type",
                          "ip_allowlist",
                          "created_at",
                          "updated_at",
                          "webhook_count"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "webhook",
                              "ai_trigger"
                            ]
                          },
                          "ip_allowlist": {
                            "type": [
                              "array",
                              "null"
                            ],
                            "items": {
                              "type": "string"
                            },
                            "description": "Parsed from the stored JSON, or null when unset."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "webhook_count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed (only GET and POST are routed on this path).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Readable by any role.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "operationId": "createProject",
        "summary": "Create a project",
        "tags": [
          "Projects"
        ],
        "responses": {
          "200": {
            "description": "Created. Note the status is 200, not 201. The slug is generated from the name plus a random salt. For a webhook project the seeded endpoint is returned; its slug is the high-entropy public URL secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "project"
                  ],
                  "properties": {
                    "project": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "slug",
                        "type",
                        "ip_allowlist",
                        "created_at",
                        "updated_at",
                        "webhook_count"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "webhook",
                            "ai_trigger"
                          ]
                        },
                        "ip_allowlist": {
                          "type": [
                            "array",
                            "null"
                          ],
                          "items": {
                            "type": "string"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "webhook_count": {
                          "type": "integer"
                        }
                      }
                    },
                    "webhook": {
                      "type": "object",
                      "description": "Present only for type=webhook.",
                      "required": [
                        "id",
                        "slug",
                        "name"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string",
                          "description": "High-entropy public webhook slug (the URL credential)."
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "name is required (1-80 chars), or type must be webhook or ai_trigger.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role - project creation is owner/admin only, so a developer is refused here even though it may write project resources; also returned for a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Owner/admin only.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "type"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80,
                    "description": "Trimmed; must be non-empty and at most 80 characters."
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "webhook",
                      "ai_trigger"
                    ],
                    "description": "A webhook project is seeded with one enabled webhook so it has a URL immediately."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}": {
      "get": {
        "operationId": "getProject",
        "summary": "Get one project with resource counts",
        "tags": [
          "Projects"
        ],
        "responses": {
          "200": {
            "description": "The project plus per-resource counts for the tabbed view.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "project"
                  ],
                  "properties": {
                    "project": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "slug",
                        "type",
                        "ip_allowlist",
                        "created_at",
                        "updated_at",
                        "counts"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "webhook",
                            "ai_trigger"
                          ]
                        },
                        "ip_allowlist": {
                          "type": [
                            "array",
                            "null"
                          ],
                          "items": {
                            "type": "string"
                          }
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "counts": {
                          "type": "object",
                          "required": [
                            "webhooks",
                            "rules",
                            "ingest_keys",
                            "destinations",
                            "sources",
                            "triggers"
                          ],
                          "properties": {
                            "webhooks": {
                              "type": "integer"
                            },
                            "rules": {
                              "type": "integer"
                            },
                            "ingest_keys": {
                              "type": "integer"
                            },
                            "destinations": {
                              "type": "integer"
                            },
                            "sources": {
                              "type": "integer"
                            },
                            "triggers": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found (also returned for a project belonging to another workspace - existence is never leaked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "operationId": "updateProject",
        "summary": "Rename a project or set its IP allowlist",
        "tags": [
          "Projects"
        ],
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No supported fields to update (neither an acceptable name nor an ip_allowlist array/null was supplied).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role - owner/admin only; also returned for a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Owner/admin only. Slug and type are immutable.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80,
                    "description": "Applied only when it is a non-empty string of at most 80 characters; otherwise silently skipped."
                  },
                  "ip_allowlist": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string"
                    },
                    "description": "Stored as JSON verbatim when an array, or cleared when null. This handler does NOT run the CIDR validator, unlike PATCH /admin/api/ip-allowlist."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "delete": {
        "operationId": "deleteProject",
        "summary": "Delete a project",
        "tags": [
          "Projects"
        ],
        "responses": {
          "200": {
            "description": "Deleted. Config rows (webhooks, rules, ingest keys, sources, triggers, workflows, portals, AI agents) are removed; records, submissions, deliveries, agent runs and destinations are kept with project_id set to NULL so history and foreign keys stay intact. Returns ok:true even when no project matched - the handler does not check the affected row count, so a missing id is not a 404.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Cannot delete the default project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Only owners can delete projects; also returned for a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Owner only. An agent can never reach owner (manage scope clips to admin).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/stats": {
      "get": {
        "operationId": "getProjectStats",
        "summary": "Traffic and delivery stats for one project",
        "tags": [
          "Stats"
        ],
        "responses": {
          "200": {
            "description": "Event volumes, 7-day delivery health, a zero-filled 14-day daily series and the top datasets of the last 7 days.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "events",
                    "submissions_7d",
                    "deliveries",
                    "ai_runs_7d",
                    "daily",
                    "top_datasets"
                  ],
                  "properties": {
                    "events": {
                      "type": "object",
                      "required": [
                        "last_24h",
                        "last_7d",
                        "last_30d"
                      ],
                      "properties": {
                        "last_24h": {
                          "type": "integer"
                        },
                        "last_7d": {
                          "type": "integer"
                        },
                        "last_30d": {
                          "type": "integer"
                        }
                      }
                    },
                    "submissions_7d": {
                      "type": "integer"
                    },
                    "deliveries": {
                      "type": "object",
                      "required": [
                        "total_7d",
                        "delivered_7d",
                        "failed_7d",
                        "success_rate",
                        "avg_ms",
                        "in_flight_7d"
                      ],
                      "properties": {
                        "total_7d": {
                          "type": "integer"
                        },
                        "delivered_7d": {
                          "type": "integer"
                        },
                        "failed_7d": {
                          "type": "integer",
                          "description": "Counts both failed and dead."
                        },
                        "success_rate": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "delivered_7d / (delivered_7d + failed_7d) as a whole percent: a share of FINISHED deliveries. null until one has finished."
                        },
                        "avg_ms": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "in_flight_7d": {
                          "type": "integer",
                          "description": "Queued, retrying or held by a paused destination - no outcome yet."
                        }
                      }
                    },
                    "ai_runs_7d": {
                      "type": "integer"
                    },
                    "daily": {
                      "type": "array",
                      "description": "Exactly 14 entries, oldest first, zero-filled.",
                      "items": {
                        "type": "object",
                        "required": [
                          "day",
                          "events"
                        ],
                        "properties": {
                          "day": {
                            "type": "string",
                            "description": "YYYY-MM-DD (UTC)."
                          },
                          "events": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "top_datasets": {
                      "type": "array",
                      "description": "At most 5.",
                      "items": {
                        "type": "object",
                        "required": [
                          "dataset",
                          "n"
                        ],
                        "properties": {
                          "dataset": {
                            "type": "string"
                          },
                          "n": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Nested form only - stats has NO top-level Default-project alias (GET /admin/api/stats returns 404 Unknown resource). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/datasets": {
      "get": {
        "operationId": "listDatasets",
        "summary": "List datasets with record counts",
        "tags": [
          "Datasets"
        ],
        "responses": {
          "200": {
            "description": "One row per dataset that has at least one record in this project, ordered by name.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "datasets"
                  ],
                  "properties": {
                    "datasets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "dataset",
                          "count",
                          "last"
                        ],
                        "properties": {
                          "dataset": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          },
                          "last": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "Most recent received_at in the dataset."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/datasets (same handler, scoped to the workspace Default project). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/datasets/{dataset}": {
      "get": {
        "operationId": "exportDatasetRecords",
        "summary": "One page of a dataset's records, whole and flattened (export shape)",
        "tags": [
          "Datasets"
        ],
        "responses": {
          "200": {
            "description": "At most `limit` newest records after the cursor, payload JSON flattened into rows with the meta columns _id, _received_at and _source prepended. columns is the union of the meta columns and every payload key seen ON THIS PAGE. A non-object payload becomes a single value key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "dataset",
                    "columns",
                    "rows",
                    "next_cursor"
                  ],
                  "properties": {
                    "dataset": {
                      "type": "string"
                    },
                    "columns": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true,
                        "required": [
                          "_id",
                          "_received_at",
                          "_source"
                        ],
                        "properties": {
                          "_id": {
                            "type": "string"
                          },
                          "_received_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "_source": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Opaque; pass it back as ?cursor= for the next page. null on the last page."
                    },
                    "total": {
                      "type": "integer",
                      "description": "Records in the dataset. On the first page only (no cursor)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Dataset name must match the identifier pattern; limit outside 1-1000; or a cursor this API did not issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/datasets/{dataset}. Keyset-paged, newest first, so an export can cover the WHOLE dataset in bounded requests: follow next_cursor until it is null. The cursor is a position (received_at, id), so records arriving during an export shift nothing, and a failed page can be retried with the same cursor. An unknown-but-valid dataset name returns 200 with empty rows, not 404. Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dataset",
            "in": "path",
            "required": true,
            "description": "Dataset name; must match ^[a-z][a-z0-9_]{0,62}$ (case-insensitive).",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page, 1-1000. Default 1000.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 1000
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "next_cursor from the previous page, passed back exactly. Omit for the first page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/records/{id}": {
      "get": {
        "operationId": "getRecord",
        "summary": "Get one record with its full payload",
        "tags": [
          "Records"
        ],
        "responses": {
          "200": {
            "description": "The record, payload parsed as JSON (a non-object payload is wrapped as {value: ...}).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "record"
                  ],
                  "properties": {
                    "record": {
                      "type": "object",
                      "required": [
                        "id",
                        "dataset",
                        "source",
                        "received_at",
                        "webhook_id",
                        "submission_id",
                        "payload"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "source": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "received_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "webhook_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "submission_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "payload": {
                          "type": "object",
                          "additionalProperties": true
                        },
                        "request": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "description": "The HTTP request the event arrived in (ING-3, #193). null for an event stored before #193, or one that did not arrive over HTTP (a polled database row, a cron or WebSocket trigger). Credentials are never stored: Authorization, Proxy-Authorization, Cookie and any header or query parameter named like a credential (key, api-key, token, secret, password, session, auth) is stored as \"[redacted]\"; Cloudflare and proxy hop headers (cf-*, x-forwarded-*, x-real-ip, true-client-ip, cdn-loop, host) are dropped.",
                          "required": [
                            "method",
                            "headers",
                            "query",
                            "content_type",
                            "source_ip"
                          ],
                          "properties": {
                            "method": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "example": "POST"
                            },
                            "headers": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "string"
                              },
                              "description": "Lowercased header names. At most 64 headers, each value cut at 1024 characters, about 8 KB in all.",
                              "example": {
                                "x-github-event": "push",
                                "user-agent": "GitHub-Hookshot/abc",
                                "authorization": "[redacted]"
                              }
                            },
                            "query": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "The query string without its '?', at most 2048 characters.",
                              "example": "topic=orders"
                            },
                            "content_type": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "The Content-Type header as sent, parameters included."
                            },
                            "source_ip": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "CF-Connecting-IP."
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record not found (also for a record in another project or workspace). The same path with any method other than GET or DELETE returns 404 with error: Record id required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/records/{id}. GET /admin/api/projects/{project_id}/records (no id) is not a route - it returns 404 Record id required. Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Record id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "delete": {
        "operationId": "deleteRecord",
        "summary": "Delete a record and every copy of it",
        "tags": [
          "Records"
        ],
        "description": "Permanently deletes one record and every copy Hookie keeps of it, in one batch: its raw submission, its pending fan-out, its deliveries (replays included) with their response bodies and every failed attempt kept for them, its held deliveries, the workflow runs it started (live Cloudflare runs are terminated first) with their step log, waiters and AI calls, and for AI trigger runs on it the AI calls and the model's output (the run row stays as the monthly AI-run quota ledger; a run still queued is failed). Deliveries already sent are not recalled. Audited as `delete_record` with the counts, never the payload. Developer role and up; also the MCP tool `delete_record`. Top-level alias: DELETE /admin/api/records/{id} (Default project).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Record id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "deleted"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "deleted": {
                      "$ref": "#/components/schemas/EraseCounts"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (viewer), `workspace_suspended`, or `Missing X-Requested-With header`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Record not found \u2014 also for a record in another project or workspace, and for one already deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/submissions": {
      "get": {
        "operationId": "listSubmissions",
        "summary": "List raw submissions",
        "tags": [
          "Submissions"
        ],
        "responses": {
          "200": {
            "description": "The 500 newest submissions of the project. payload is the stored body truncated to its first 4000 characters (a string, not parsed JSON). No pagination parameters are supported on this route - use POST search/submissions for filtering and paging.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "submissions"
                  ],
                  "properties": {
                    "submissions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "received_at",
                          "source",
                          "forward_status",
                          "forwarded_at",
                          "payload"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "received_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "source": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "forward_status": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "pending",
                              "sent",
                              "failed",
                              "filtered",
                              null
                            ],
                            "description": "pending: not routed yet (or, before #287, rejected by an endpoint's criteria); sent: routed into records; failed: routing threw and the outbox sweep retries it; filtered: an endpoint's own criteria rejected it (#287), terminal."
                          },
                          "forwarded_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "payload": {
                            "type": "string",
                            "description": "Truncated to 4000 characters."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/submissions. Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/submissions/{id}": {
      "patch": {
        "operationId": "updateSubmissionForwardStatus",
        "summary": "Set a submission forward status",
        "tags": [
          "Submissions"
        ],
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "forward_status must be pending, sent or failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Submission not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: PATCH /admin/api/submissions/{id}. Write role required. PATCH is the only method on this path: there is no GET-one-submission route, and any other method answers 405 with `Allow: PATCH` (#277). Setting a status by hand clears the reason routing gave the submission (the Errors tab's `reason`, #287). `filtered` is set only by routing.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Submission id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "forward_status"
                ],
                "properties": {
                  "forward_status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "sent",
                      "failed"
                    ],
                    "description": "Setting it to sent also stamps forwarded_at; any other value leaves forwarded_at untouched."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/rules": {
      "get": {
        "operationId": "listRules",
        "summary": "List mapping rules",
        "tags": [
          "Rules"
        ],
        "responses": {
          "200": {
            "description": "User-authored rules only (the auto-generated per-webhook mirror rules, which carry webhook_id, are excluded). Newest first. conditions and mappings come back as JSON-encoded strings exactly as stored, and enabled is 0 or 1.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rules"
                  ],
                  "properties": {
                    "rules": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "enabled",
                          "conditions",
                          "dataset",
                          "mappings",
                          "created_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "enabled": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ]
                          },
                          "conditions": {
                            "type": "string",
                            "description": "JSON array of conditions as stored: an exact match as {path, equals}, every other operator as {path, op, value} (see Condition)."
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "mappings": {
                            "type": "string",
                            "description": "JSON array of {path, key}."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/rules. Readable by any role. Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "operationId": "createRule",
        "summary": "Create a mapping rule",
        "tags": [
          "Rules"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "rule": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "enabled",
                        "conditions",
                        "dataset",
                        "mappings",
                        "created_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "conditions": {
                          "type": "string",
                          "description": "JSON array of {path, equals}."
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "mappings": {
                          "type": "string",
                          "description": "JSON array of {path, key}."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      },
                      "description": "The created rule, as GET by id returns it (#201)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failure - body not a JSON object, name missing or over 80 characters, dataset not an identifier, a malformed condition or mapping, too many conditions (max 10) or mappings (max 50), or a duplicate mapping key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/rules. Write role required.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "dataset"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80,
                    "description": "Trimmed before storing; must be non-empty and at most 80 characters."
                  },
                  "dataset": {
                    "type": "string",
                    "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$",
                    "description": "The logical table matched events land in."
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": true,
                    "description": "Anything other than the literal false is treated as true."
                  },
                  "conditions": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Condition"
                    },
                    "maxItems": 10,
                    "description": "Conditions in the one dialect (see Condition); equals, not_equals and in compare as text. A path has no whitespace, does not begin with a dot and is not a $-special path. Empty or omitted = match every event."
                  },
                  "mappings": {
                    "type": "array",
                    "maxItems": 50,
                    "description": "Omitted, null or empty means identity - store the whole payload.",
                    "items": {
                      "type": "object",
                      "required": [
                        "path",
                        "key"
                      ],
                      "properties": {
                        "path": {
                          "type": "string",
                          "maxLength": 200,
                          "description": "Payload field path, or one of the specials $payload, $submission.id, $submission.received_at."
                        },
                        "key": {
                          "type": "string",
                          "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$",
                          "description": "Output key; must be unique within the rule, compared case-insensitively."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/rules/{id}": {
      "get": {
        "operationId": "getRule",
        "summary": "Get one rule",
        "tags": [
          "Rules"
        ],
        "description": "The object, as one row of the list returns it, or 404. This used to answer 200 with the whole list for any id (DX-6, #201). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Rule id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rule"
                  ],
                  "properties": {
                    "rule": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "enabled",
                        "conditions",
                        "dataset",
                        "mappings",
                        "created_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "conditions": {
                          "type": "string",
                          "description": "JSON array of {path, equals}."
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "mappings": {
                          "type": "string",
                          "description": "JSON array of {path, key}."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Rule not found in this project, or it is a webhook mirror rule (those are not addressable here).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateRule",
        "summary": "Update a rule",
        "tags": [
          "Rules"
        ],
        "responses": {
          "200": {
            "description": "Updated. The rule as GET by id returns it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "rule"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "rule": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "enabled",
                        "conditions",
                        "dataset",
                        "mappings",
                        "created_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "conditions": {
                          "type": "string",
                          "description": "JSON array of conditions as stored: an exact match as {path, equals}, every other operator as {path, op, value} (see Condition)."
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "mappings": {
                          "type": "string",
                          "description": "JSON array of {path, key}."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body not an object, an unknown field, enabled not a boolean, or a spec a create would refuse (the message names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Rule not found in this project, or it is a webhook mirror rule (those are not addressable here).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Change any of enabled, name, dataset, conditions and mappings in place (ING-12, #201); it used to toggle enabled only. Send only what changes; the result is validated as a create is. Top-level alias: PATCH /admin/api/rules/{id}. Write role required.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Rule id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "additionalProperties": false,
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "dataset": {
                    "type": "string"
                  },
                  "conditions": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Condition"
                    },
                    "maxItems": 10
                  },
                  "mappings": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "path",
                        "key"
                      ],
                      "properties": {
                        "path": {
                          "type": "string"
                        },
                        "key": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "delete": {
        "operationId": "deleteRule",
        "summary": "Delete a rule",
        "tags": [
          "Rules"
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Rule not found in this project, or it is a webhook mirror rule.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: DELETE /admin/api/rules/{id}. Write role required.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Rule id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "summary": "List endpoints",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "All webhook versions in the project, ordered by base_slug then version. slug is the high-entropy public URL credential used at app.hookie.ai/{workspace}/{project}/{slug}; base_slug plus version is the human identity. criteria, mappings and ip_allowlist are JSON-encoded strings as stored. After a rotation, previous_public_url is the replaced URL, still accepted until previous_url_expires_at; both are null outside an overlap. verification is the endpoint's signature check: scheme, settings, has_secret and previous_secret_expires_at, never the secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "webhooks"
                  ],
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "base_slug",
                          "version",
                          "name",
                          "enabled",
                          "criteria",
                          "dataset",
                          "mappings",
                          "slug",
                          "require_signature",
                          "ip_allowlist",
                          "created_at",
                          "redirect_url",
                          "cors_origins",
                          "handshake",
                          "handshake_secret_set"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "base_slug": {
                            "type": "string",
                            "description": "The readable identity you supplied. NOT the public URL segment. Safe to log.",
                            "example": "orders"
                          },
                          "version": {
                            "type": "string",
                            "description": "Major, or major.minor."
                          },
                          "name": {
                            "type": "string"
                          },
                          "enabled": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ]
                          },
                          "criteria": {
                            "type": "string",
                            "description": "JSON array of {path, equals}."
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "mappings": {
                            "type": "string",
                            "description": "JSON array of {path, key}."
                          },
                          "slug": {
                            "type": "string",
                            "description": "The high-entropy public URL segment. THIS IS THE CREDENTIAL \u2014 anyone holding it can post events. Never log it.",
                            "example": "aBcXyz0123456789abcdefgh"
                          },
                          "require_signature": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ],
                            "description": "Legacy (0001): checks Hookie's own X-Hookie-Signature and is always 0; no route sets it. Provider signatures are `verification`."
                          },
                          "ip_allowlist": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The allowlist as stored: a JSON array of IPs/CIDRs encoded as a string, or null for any address."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "webhook_slug": {
                            "type": "string",
                            "description": "The high-entropy public URL segment. THIS IS THE CREDENTIAL \u2014 anyone holding it can post events. Never log it.",
                            "example": "aBcXyz0123456789abcdefgh"
                          },
                          "public_path": {
                            "type": "string",
                            "description": "The endpoint's public location, `{workspace}/{project}/{webhook_slug}`. Pass this straight to a POST; it needs no assembly. It contains the credential, so treat it as a secret.",
                            "example": "acme-3f9a/orders/aBcXyz0123456789abcdefgh"
                          },
                          "public_url": {
                            "type": "string",
                            "format": "uri",
                            "description": "`public_path` as an absolute URL on this deployment. Contains the credential \u2014 treat it as a secret.",
                            "example": "https://app.hookie.ai/acme-3f9a/orders/aBcXyz0123456789abcdefgh"
                          },
                          "previous_public_url": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "uri",
                            "description": "The URL a rotation replaced, while it is still accepted. It contains the old credential - treat it as a secret."
                          },
                          "previous_url_expires_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "When previous_public_url stops being accepted (404)."
                          },
                          "verification": {
                            "$ref": "#/components/schemas/EndpointVerification"
                          },
                          "redirect_url": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "ING-8 (#193): where a browser form POST is sent once stored."
                          },
                          "cors_origins": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "ING-8 (#193): origins allowed to fetch() the URL. Empty: none."
                          },
                          "handshake": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "slack",
                              "meta",
                              "graph",
                              "zoom",
                              "twitch",
                              null
                            ],
                            "description": "ING-10 (#193): the URL-verification challenge the endpoint answers."
                          },
                          "handshake_secret_set": {
                            "type": "boolean",
                            "description": "Whether a handshake secret is stored. The secret itself is never returned."
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/webhooks. Readable by any role. Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "operationId": "createWebhook",
        "summary": "Create an endpoint",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "201": {
            "description": "Created at version 1, with a mirror mapping rule synced behind it. webhook_slug is the public URL credential and is the only place it is returned in full at creation time.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "slug",
                    "webhook_slug"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string",
                      "description": "DEPRECATED shape: the versioned readable id, `<base_slug>-v<version>`. Matches no column and never appears in a URL. Use base_slug + version, or webhook_slug for the URL.",
                      "example": "orders-v1"
                    },
                    "webhook_slug": {
                      "type": "string",
                      "description": "The high-entropy public URL segment. THIS IS THE CREDENTIAL \u2014 anyone holding it can post events. Never log it.",
                      "example": "aBcXyz0123456789abcdefgh"
                    },
                    "base_slug": {
                      "type": "string",
                      "description": "The readable identity you supplied. NOT the public URL segment. Safe to log.",
                      "example": "orders"
                    },
                    "version": {
                      "type": "string",
                      "description": "Endpoint version within `base_slug`.",
                      "example": "1"
                    },
                    "public_path": {
                      "type": "string",
                      "description": "The endpoint's public location, `{workspace}/{project}/{webhook_slug}`. Pass this straight to a POST; it needs no assembly. It contains the credential, so treat it as a secret.",
                      "example": "acme-3f9a/orders/aBcXyz0123456789abcdefgh"
                    },
                    "public_url": {
                      "type": "string",
                      "format": "uri",
                      "description": "`public_path` as an absolute URL on this deployment. Contains the credential \u2014 treat it as a secret.",
                      "example": "https://app.hookie.ai/acme-3f9a/orders/aBcXyz0123456789abcdefgh"
                    },
                    "verification": {
                      "$ref": "#/components/schemas/EndpointVerification"
                    },
                    "webhook": {
                      "type": "object",
                      "required": [
                        "id",
                        "base_slug",
                        "version",
                        "name",
                        "enabled",
                        "criteria",
                        "dataset",
                        "mappings",
                        "slug",
                        "require_signature",
                        "ip_allowlist",
                        "created_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "base_slug": {
                          "type": "string",
                          "description": "The readable identity you supplied. NOT the public URL segment. Safe to log.",
                          "example": "orders"
                        },
                        "version": {
                          "type": "string",
                          "description": "Major, or major.minor."
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "criteria": {
                          "type": "string",
                          "description": "JSON array of {path, equals}."
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "mappings": {
                          "type": "string",
                          "description": "JSON array of {path, key}."
                        },
                        "slug": {
                          "type": "string",
                          "description": "The high-entropy public URL segment. THIS IS THE CREDENTIAL \u2014 anyone holding it can post events. Never log it.",
                          "example": "aBcXyz0123456789abcdefgh"
                        },
                        "require_signature": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "ip_allowlist": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The allowlist as stored: a JSON array of IPs/CIDRs encoded as a string, or null for any address."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "webhook_slug": {
                          "type": "string",
                          "description": "The high-entropy public URL segment. THIS IS THE CREDENTIAL \u2014 anyone holding it can post events. Never log it.",
                          "example": "aBcXyz0123456789abcdefgh"
                        },
                        "public_path": {
                          "type": "string",
                          "description": "The endpoint's public location, `{workspace}/{project}/{webhook_slug}`. Pass this straight to a POST; it needs no assembly. It contains the credential, so treat it as a secret.",
                          "example": "acme-3f9a/orders/aBcXyz0123456789abcdefgh"
                        },
                        "public_url": {
                          "type": "string",
                          "format": "uri",
                          "description": "`public_path` as an absolute URL on this deployment. Contains the credential \u2014 treat it as a secret.",
                          "example": "https://app.hookie.ai/acme-3f9a/orders/aBcXyz0123456789abcdefgh"
                        },
                        "previous_public_url": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "uri",
                          "description": "The URL a rotation replaced, while it is still accepted. It contains the old credential - treat it as a secret."
                        },
                        "previous_url_expires_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "When previous_public_url stops being accepted (404)."
                        }
                      },
                      "description": "The created webhook, as GET by id returns it (#201)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failure - name, slug pattern, dataset identifier, criteria or mappings, or ip_allowlist is not an array of valid IPs/CIDRs (at most 64), or an invalid verification (unknown scheme, missing secret, a setting that does not apply to the scheme).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Plan limit reached (10 webhooks per project).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "An endpoint with that slug already exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/webhooks. Write role required. The cap is checked before validation, so a full project returns 402 even for an invalid body. `verification` optionally makes the endpoint check its provider's signature (#210); the create response echoes the scheme, never the secret.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "slug": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,46}$",
                    "description": "The base slug (lowercase letters, digits and hyphens, max 47 characters). This is NOT the public URL - a separate high-entropy slug is generated and returned as webhook_slug. Optional: when omitted or empty it is derived from the name (accents folded, Cyrillic and Greek transliterated) plus a 4-character random suffix, and a name with no Latin spelling becomes endpoint-<random>."
                  },
                  "dataset": {
                    "type": "string",
                    "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$",
                    "description": "Optional: when omitted or empty, the name as an identifier (folded to ASCII, d_ prefixed if it starts with a digit), or default."
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": true,
                    "description": "Anything other than the literal false is treated as true."
                  },
                  "criteria": {
                    "type": "array",
                    "maxItems": 10,
                    "description": "Omitted or empty accepts any payload (the URL already scopes it). At most 10, the same limit as a rule's conditions; more is 400 \"At most 10 criteria are allowed\".",
                    "items": {
                      "$ref": "#/components/schemas/Condition"
                    }
                  },
                  "mappings": {
                    "type": "array",
                    "maxItems": 50,
                    "description": "Omitted or empty means identity - store the whole payload.",
                    "items": {
                      "type": "object",
                      "required": [
                        "path",
                        "key"
                      ],
                      "properties": {
                        "path": {
                          "type": "string",
                          "maxLength": 200,
                          "description": "Payload field path, or $payload, $submission.id, $submission.received_at."
                        },
                        "key": {
                          "type": "string",
                          "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$"
                        }
                      }
                    }
                  },
                  "ip_allowlist": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "maxItems": 64,
                    "items": {
                      "type": "string",
                      "description": "An IPv4 or IPv6 address, or a CIDR range."
                    },
                    "description": "IP addresses or CIDR ranges allowed to use this credential, validated exactly as the workspace and project allowlists are. [] or null removes the restriction (stored as null). A request must pass every allowlist that is set: this one, the project's and the workspace's. Refused requests get 403 'Source IP not allowed' and an ingest_ip_blocked audit row.",
                    "example": [
                      "203.0.113.0/24",
                      "2001:db8::/32"
                    ]
                  },
                  "verification": {
                    "$ref": "#/components/schemas/EndpointVerificationInput"
                  },
                  "redirect_url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "ING-8: an https:// URL (no user:password) a browser's native form POST is sent to (303) once stored. null or \"\" clears it.",
                    "example": "https://example.com/thanks"
                  },
                  "cors_origins": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "maxItems": 20,
                    "items": {
                      "type": "string",
                      "description": "An origin - scheme, host and optional port, no path - or \"*\" for any page."
                    },
                    "description": "ING-8: origins whose fetch() may call the endpoint URL and read the answer, OPTIONS preflight included. [] or null allows none, which is the default. Stored normalised (a trailing slash dropped).",
                    "example": [
                      "https://example.com"
                    ]
                  },
                  "handshake": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "slack",
                      "meta",
                      "graph",
                      "zoom",
                      "twitch",
                      null
                    ],
                    "description": "ING-10: the provider URL-verification challenge the endpoint answers without storing it. null answers none. Choosing one that takes no secret clears any stored handshake_secret."
                  },
                  "handshake_secret": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 256,
                    "writeOnly": true,
                    "description": "Required with handshake zoom (the app's Secret Token, which signs the answer) or meta (the Verify Token compared with hub.verify_token), unless one is already stored for that same handshake. Refused with any other handshake. WRITE-ONLY: AES-256-GCM encrypted, never returned, never in the audit log (which records only that one was set)."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/webhooks/{id}/versions": {
      "post": {
        "operationId": "createWebhookVersion",
        "summary": "Publish a new version of a webhook",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "201": {
            "description": "New version created with its own high-entropy public slug (not returned here - read it back from the webhook list) and its own mirror rule. The per-project webhook cap is NOT re-checked on this path.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "slug"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string",
                      "description": "DEPRECATED shape: the versioned readable id, `<base_slug>-v<version>`. Matches no column and never appears in a URL. Use base_slug + version, or webhook_slug for the URL.",
                      "example": "orders-v1"
                    },
                    "base_slug": {
                      "type": "string",
                      "description": "The readable identity you supplied. NOT the public URL segment. Safe to log.",
                      "example": "orders"
                    },
                    "version": {
                      "type": "string",
                      "description": "Endpoint version within `base_slug`.",
                      "example": "1"
                    },
                    "webhook_slug": {
                      "type": "string",
                      "description": "The high-entropy public URL segment. THIS IS THE CREDENTIAL \u2014 anyone holding it can post events. Never log it.",
                      "example": "aBcXyz0123456789abcdefgh"
                    },
                    "public_path": {
                      "type": "string",
                      "description": "The endpoint's public location, `{workspace}/{project}/{webhook_slug}`. Pass this straight to a POST; it needs no assembly. It contains the credential, so treat it as a secret.",
                      "example": "acme-3f9a/orders/aBcXyz0123456789abcdefgh"
                    },
                    "public_url": {
                      "type": "string",
                      "format": "uri",
                      "description": "`public_path` as an absolute URL on this deployment. Contains the credential \u2014 treat it as a secret.",
                      "example": "https://app.hookie.ai/acme-3f9a/orders/aBcXyz0123456789abcdefgh"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Spec validation failure, or slug must match the base endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Endpoint not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/webhooks/{id}/versions. Write role required. The new version carries the source version's signature verification (scheme, settings and secret).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of any existing webhook row sharing the base slug to version.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "slug",
                  "dataset"
                ],
                "description": "A complete webhook spec (same validator as create) plus the optional major flag.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "slug": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{0,46}$",
                    "description": "Must equal the base slug of the webhook named by {id}."
                  },
                  "dataset": {
                    "type": "string",
                    "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$"
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": true
                  },
                  "criteria": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "$ref": "#/components/schemas/Condition"
                    }
                  },
                  "mappings": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "object",
                      "required": [
                        "path",
                        "key"
                      ],
                      "properties": {
                        "path": {
                          "type": "string",
                          "maxLength": 200
                        },
                        "key": {
                          "type": "string",
                          "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$"
                        }
                      }
                    }
                  },
                  "major": {
                    "type": "boolean",
                    "default": false,
                    "description": "true bumps the major version (2), otherwise the minor is incremented (1.1)."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/webhooks/{id}": {
      "get": {
        "operationId": "getWebhook",
        "summary": "Get one webhook",
        "tags": [
          "Webhooks"
        ],
        "description": "The object, as one row of the list returns it, or 404. This used to answer 200 with the whole list for any id (DX-6, #201). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "webhook"
                  ],
                  "properties": {
                    "webhook": {
                      "type": "object",
                      "required": [
                        "id",
                        "base_slug",
                        "version",
                        "name",
                        "enabled",
                        "criteria",
                        "dataset",
                        "mappings",
                        "slug",
                        "require_signature",
                        "ip_allowlist",
                        "created_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "base_slug": {
                          "type": "string",
                          "description": "The readable identity you supplied. NOT the public URL segment. Safe to log.",
                          "example": "orders"
                        },
                        "version": {
                          "type": "string",
                          "description": "Major, or major.minor."
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "criteria": {
                          "type": "string",
                          "description": "JSON array of {path, equals}."
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "mappings": {
                          "type": "string",
                          "description": "JSON array of {path, key}."
                        },
                        "slug": {
                          "type": "string",
                          "description": "The high-entropy public URL segment. THIS IS THE CREDENTIAL \u2014 anyone holding it can post events. Never log it.",
                          "example": "aBcXyz0123456789abcdefgh"
                        },
                        "require_signature": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "ip_allowlist": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The allowlist as stored: a JSON array of IPs/CIDRs encoded as a string, or null for any address."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "webhook_slug": {
                          "type": "string",
                          "description": "The high-entropy public URL segment. THIS IS THE CREDENTIAL \u2014 anyone holding it can post events. Never log it.",
                          "example": "aBcXyz0123456789abcdefgh"
                        },
                        "public_path": {
                          "type": "string",
                          "description": "The endpoint's public location, `{workspace}/{project}/{webhook_slug}`. Pass this straight to a POST; it needs no assembly. It contains the credential, so treat it as a secret.",
                          "example": "acme-3f9a/orders/aBcXyz0123456789abcdefgh"
                        },
                        "public_url": {
                          "type": "string",
                          "format": "uri",
                          "description": "`public_path` as an absolute URL on this deployment. Contains the credential \u2014 treat it as a secret.",
                          "example": "https://app.hookie.ai/acme-3f9a/orders/aBcXyz0123456789abcdefgh"
                        },
                        "previous_public_url": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "uri",
                          "description": "The URL a rotation replaced, while it is still accepted. It contains the old credential - treat it as a secret."
                        },
                        "previous_url_expires_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "When previous_public_url stops being accepted (404)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Endpoint not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateWebhook",
        "summary": "Change an endpoint: enabled, name, dataset, IP allowlist, form redirect, CORS origins, URL verification, signature verification",
        "tags": [
          "Webhooks"
        ],
        "description": "Top-level alias: PATCH /admin/api/webhooks/{id}. Write role required. Send at least one field; only those sent change, and the endpoint keeps its id and its URL. A new dataset applies to events from now on; records already stored keep the dataset they were filed under. The mirror mapping rule the legacy /v1/webhooks/{ingest_key}/{slug} route reads moves with name, dataset and enabled in the same batch. The URL is changed with POST .../rotate; criteria and mappings are not editable here (publish a new version). Since #193 it also sets redirect_url, cors_origins, handshake and handshake_secret (see POST .../webhooks); the audit row records them, and for the secret only that one was set. `verification` sets, changes or removes provider signature verification (#210): omit its secret to keep the one held (same scheme only); a new secret replaces the old at once and ends any rotation overlap; {scheme: \"none\"} turns it off and deletes the secret. Audited as update_webhook with each change's from and to (for verification: the scheme and secret_changed, never the secret).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "additionalProperties": false,
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "While false, the endpoint's URL (and the key+slug route) answers 503 {reason: 'endpoint_disabled'} and stores nothing."
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "dataset": {
                    "type": "string",
                    "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$",
                    "description": "Where events that arrive from now on are filed."
                  },
                  "ip_allowlist": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "maxItems": 64,
                    "items": {
                      "type": "string",
                      "description": "An IPv4 or IPv6 address, or a CIDR range."
                    },
                    "description": "IP addresses or CIDR ranges allowed to use this credential, validated exactly as the workspace and project allowlists are. [] or null removes the restriction (stored as null). A request must pass every allowlist that is set: this one, the project's and the workspace's. Refused requests get 403 'Source IP not allowed' and an ingest_ip_blocked audit row.",
                    "example": [
                      "203.0.113.0/24",
                      "2001:db8::/32"
                    ]
                  },
                  "verification": {
                    "$ref": "#/components/schemas/EndpointVerificationInput"
                  },
                  "redirect_url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "ING-8: an https:// URL (no user:password) a browser's native form POST is sent to (303) once stored. null or \"\" clears it.",
                    "example": "https://example.com/thanks"
                  },
                  "cors_origins": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "maxItems": 20,
                    "items": {
                      "type": "string",
                      "description": "An origin - scheme, host and optional port, no path - or \"*\" for any page."
                    },
                    "description": "ING-8: origins whose fetch() may call the endpoint URL and read the answer, OPTIONS preflight included. [] or null allows none, which is the default. Stored normalised (a trailing slash dropped).",
                    "example": [
                      "https://example.com"
                    ]
                  },
                  "handshake": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "slack",
                      "meta",
                      "graph",
                      "zoom",
                      "twitch",
                      null
                    ],
                    "description": "ING-10: the provider URL-verification challenge the endpoint answers without storing it. null answers none. Choosing one that takes no secret clears any stored handshake_secret."
                  },
                  "handshake_secret": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 256,
                    "writeOnly": true,
                    "description": "Required with handshake zoom (the app's Secret Token, which signs the answer) or meta (the Verify Token compared with hub.verify_token), unless one is already stored for that same handshake. Refused with any other handshake. WRITE-ONLY: AES-256-GCM encrypted, never returned, never in the audit log (which records only that one was set)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated. Echoes what changed (ip_allowlist as the normalised array). handshake_secret is never echoed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "enabled": {
                      "type": "boolean"
                    },
                    "name": {
                      "type": "string"
                    },
                    "dataset": {
                      "type": "string"
                    },
                    "ip_allowlist": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "verification": {
                      "$ref": "#/components/schemas/EndpointVerification"
                    },
                    "redirect_url": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "cors_origins": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "As stored, normalised."
                    },
                    "handshake": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body is not an object, sends no field, sends a field that cannot change here (slug, criteria, mappings, ...), or a field is invalid: enabled not a boolean, empty or over-long name, invalid dataset name, invalid allowlist entry. Also: redirect_url not an https URL; a cors_origins entry that is not an origin (a path, a query) or more than 20; an unknown handshake; zoom or meta without handshake_secret; handshake_secret with a handshake that takes none.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Endpoint not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "delete": {
        "operationId": "deleteWebhook",
        "summary": "Delete a webhook version",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Deleted, along with its mirror mapping rule. The public URL stops accepting events immediately.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Endpoint not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: DELETE /admin/api/webhooks/{id}. Write role required.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/datasets/{dataset}/records": {
      "get": {
        "operationId": "listDatasetRecords",
        "summary": "One page of a dataset's records, optionally through a saved view",
        "tags": [
          "Records"
        ],
        "responses": {
          "200": {
            "description": "One page of the dataset's records, newest first unless the view sorts otherwise. Without a view, every row is the record meta columns plus its whole flattened payload and columns is the discovered key set. With a view, each row carries _id plus exactly the view columns (missing keys become null), sorted in SQL by the view sort key. available_columns is always the full discovered set; total is the dataset's record count up to 10,000, and total_capped is true when there are more. A view sorted by a payload field sorts only the newest 10,000 records (sort_window); a sort by arrival covers the whole dataset.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "dataset",
                    "view",
                    "columns",
                    "available_columns",
                    "rows",
                    "count",
                    "total",
                    "total_capped",
                    "sort_window",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "dataset": {
                      "type": "string"
                    },
                    "view": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "The applied view in list shape (snake_case), or null.",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "columns": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "key": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "sort_key": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "sort_dir": {
                          "type": "string",
                          "enum": [
                            "asc",
                            "desc"
                          ]
                        },
                        "is_default": {
                          "type": "boolean"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        }
                      }
                    },
                    "columns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "key",
                          "label"
                        ],
                        "properties": {
                          "key": {
                            "type": "string"
                          },
                          "label": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "available_columns": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Includes the meta columns _id, _received_at and _source."
                    },
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "count": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Records in the dataset, counted up to 10,000 (and, for a view sorted by a payload field, at most sort_window)."
                    },
                    "total_capped": {
                      "type": "boolean",
                      "description": "True when the dataset holds more than 10,000 records, so total is a floor: show it as 10,000+."
                    },
                    "sort_window": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Set when the view sorts by a payload field: the sort covers only this many of the newest records. Null for a sort by arrival."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`View not found`: the `view` id is not a saved view of this dataset.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/webhooks/{id}/records. Readable by any role. Only GET is routed here; other methods on this path fall through the webhook router to 405.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dataset",
            "in": "path",
            "required": true,
            "description": "Dataset name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "view",
            "in": "query",
            "required": false,
            "description": "Record-view id to shape and sort the rows. The view must belong to this webhook.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Rows per page (default 25, max 1000).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 25
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip (default 0).",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/webhooks/{id}/records": {
      "get": {
        "operationId": "webhookRecordsMoved",
        "summary": "Retired: records are read by dataset",
        "deprecated": true,
        "tags": [
          "Records"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "410": {
            "description": "Records are read by dataset now. The body names the endpoint's dataset and the path to read it from.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "dataset",
                    "records_path"
                  ],
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "dataset": {
                      "type": "string"
                    },
                    "records_path": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such webhook in this project."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/record-views": {
      "get": {
        "operationId": "listRecordViews",
        "summary": "List saved record views",
        "tags": [
          "Record views"
        ],
        "responses": {
          "200": {
            "description": "Views in the project, default view first then oldest first. Field names here are snake_case (unlike the create/update responses).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "views"
                  ],
                  "properties": {
                    "views": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "dataset",
                          "name",
                          "columns",
                          "sort_key",
                          "sort_dir",
                          "is_default",
                          "created_at",
                          "updated_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "columns": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "key",
                                "label"
                              ],
                              "properties": {
                                "key": {
                                  "type": "string"
                                },
                                "label": {
                                  "type": "string"
                                }
                              }
                            }
                          },
                          "sort_key": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "sort_dir": {
                            "type": "string",
                            "enum": [
                              "asc",
                              "desc"
                            ]
                          },
                          "is_default": {
                            "type": "boolean"
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/record-views. Readable by any role. Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dataset",
            "in": "query",
            "required": false,
            "description": "Restrict to views of one dataset.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "operationId": "createRecordView",
        "summary": "Create a record view",
        "tags": [
          "Record views"
        ],
        "responses": {
          "201": {
            "description": "Created. The echoed view uses the same snake_case keys as the list (id, dataset, name, columns, sort_key, sort_dir, is_default).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "view"
                  ],
                  "properties": {
                    "view": {
                      "type": "object",
                      "required": [
                        "id",
                        "dataset",
                        "name",
                        "columns",
                        "sort_key",
                        "sort_dir",
                        "is_default"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "columns": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "key",
                              "label"
                            ],
                            "properties": {
                              "key": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "sort_key": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "sort_dir": {
                          "type": "string",
                          "enum": [
                            "asc",
                            "desc"
                          ]
                        },
                        "is_default": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "dataset is missing or not a valid dataset name (or webhook_id was sent instead), or a view-spec failure - name missing/over 80 chars, columns empty or over 40, a bad or duplicate column key, or an invalid sort_key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/record-views. Write role required.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "dataset",
                  "name",
                  "columns"
                ],
                "properties": {
                  "dataset": {
                    "type": "string",
                    "description": "The dataset this view reads (a valid dataset name). Sending webhook_id instead is a 400 that says so."
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "columns": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 40,
                    "description": "Payload keys, or the meta columns _id, _received_at, _source. Keys must be unique; a bare string is shorthand for {key, label:key}.",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string",
                          "maxLength": 128
                        },
                        {
                          "type": "object",
                          "required": [
                            "key"
                          ],
                          "properties": {
                            "key": {
                              "type": "string",
                              "maxLength": 128,
                              "description": "Trimmed, non-empty, no double quotes, backslashes or control characters."
                            },
                            "label": {
                              "type": "string",
                              "maxLength": 80,
                              "description": "Defaults to the key; longer labels are truncated to 80 characters."
                            }
                          }
                        }
                      ]
                    }
                  },
                  "sort_key": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "A column key to sort by, or null/empty for arrival order. Must satisfy the same key rules."
                  },
                  "sort_dir": {
                    "type": "string",
                    "enum": [
                      "asc",
                      "desc"
                    ],
                    "default": "desc",
                    "description": "Any value other than asc is stored as desc."
                  },
                  "is_default": {
                    "type": "boolean",
                    "default": false,
                    "description": "Only the literal true counts. Setting it clears the default flag on every other view of the same webhook in the same batch."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/record-views/{id}": {
      "get": {
        "operationId": "getView",
        "summary": "Get one view",
        "tags": [
          "Record views"
        ],
        "description": "The object, as one row of the list returns it, or 404. This used to answer 200 with the whole list for any id (DX-6, #201). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Record view id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "view"
                  ],
                  "properties": {
                    "view": {
                      "type": "object",
                      "required": [
                        "id",
                        "dataset",
                        "name",
                        "columns",
                        "sort_key",
                        "sort_dir",
                        "is_default",
                        "created_at",
                        "updated_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "columns": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "key",
                              "label"
                            ],
                            "properties": {
                              "key": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "sort_key": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "sort_dir": {
                          "type": "string",
                          "enum": [
                            "asc",
                            "desc"
                          ]
                        },
                        "is_default": {
                          "type": "boolean"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "View not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateRecordView",
        "summary": "Update a record view",
        "tags": [
          "Record views"
        ],
        "responses": {
          "200": {
            "description": "Updated. The echoed view uses the same snake_case keys as the list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "view"
                  ],
                  "properties": {
                    "view": {
                      "type": "object",
                      "required": [
                        "id",
                        "dataset",
                        "name",
                        "columns",
                        "sort_key",
                        "sort_dir",
                        "is_default"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "columns": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "required": [
                              "key",
                              "label"
                            ],
                            "properties": {
                              "key": {
                                "type": "string"
                              },
                              "label": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "sort_key": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "sort_dir": {
                          "type": "string",
                          "enum": [
                            "asc",
                            "desc"
                          ]
                        },
                        "is_default": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The merged view failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "View not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: PATCH /admin/api/record-views/{id}. Write role required.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Record view id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Supplied fields are merged over the stored view and the whole result is re-validated, so a partial body is fine but a supplied field must be complete and valid. webhook_id cannot be changed.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "columns": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 40,
                    "items": {
                      "oneOf": [
                        {
                          "type": "string",
                          "maxLength": 128
                        },
                        {
                          "type": "object",
                          "required": [
                            "key"
                          ],
                          "properties": {
                            "key": {
                              "type": "string",
                              "maxLength": 128
                            },
                            "label": {
                              "type": "string",
                              "maxLength": 80
                            }
                          }
                        }
                      ]
                    }
                  },
                  "sort_key": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "sort_dir": {
                    "type": "string",
                    "enum": [
                      "asc",
                      "desc"
                    ]
                  },
                  "is_default": {
                    "type": "boolean",
                    "description": "true clears the default flag on the webhook other views in the same batch."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "delete": {
        "operationId": "deleteRecordView",
        "summary": "Delete a record view",
        "tags": [
          "Record views"
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "View not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: DELETE /admin/api/record-views/{id}. Write role required.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Record view id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/ingest-keys": {
      "get": {
        "operationId": "listIngestKeys",
        "summary": "List ingest keys",
        "tags": [
          "Ingest keys"
        ],
        "responses": {
          "200": {
            "description": "Keys of the project, newest first. Only the prefix is stored in the clear - the key itself is held as a SHA-256 hash and can never be read back. Revoked and expired keys stay listed. status is derived: revoked when revoked_at is set, expired once expires_at has passed, otherwise active. A key with replaced_by set was rotated and stops at its expires_at.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "keys"
                  ],
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "key_prefix",
                          "dataset_default",
                          "require_signature",
                          "ip_allowlist",
                          "created_at",
                          "last_used_at",
                          "revoked_at",
                          "expires_at",
                          "replaced_by",
                          "status"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "key_prefix": {
                            "type": "string",
                            "description": "First 16 characters of the issued token: a display label only, since a key is authenticated by its full hash (#177)."
                          },
                          "dataset_default": {
                            "type": "string"
                          },
                          "require_signature": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ]
                          },
                          "ip_allowlist": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The allowlist as stored: a JSON array of IPs/CIDRs encoded as a string, or null for any address."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "last_used_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "revoked_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "expires_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "When the key stops authenticating; null means never."
                          },
                          "replaced_by": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "After a rotation, the id of the key that replaced this one."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "expired",
                              "revoked"
                            ],
                            "description": "Only an active key authenticates."
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/ingest-keys. Readable by any role. Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "operationId": "createIngestKey",
        "summary": "Issue an ingest key",
        "tags": [
          "Ingest keys"
        ],
        "responses": {
          "201": {
            "description": "Created. key is the full ik_live_ token and signing_secret the whsec_ value - both are shown exactly once and only the key hash / encrypted secret are stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "key",
                    "signing_secret"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string",
                      "description": "ik_live_ followed by 48 hex characters."
                    },
                    "signing_secret": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Present only when require_signature was true; otherwise null."
                    },
                    "key_prefix": {
                      "type": "string",
                      "description": "The first 16 characters, which is all the list shows from now on."
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "ip_allowlist": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The normalised allowlist; [] for any address."
                    },
                    "ingest_key": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "key_prefix",
                        "dataset_default",
                        "require_signature",
                        "ip_allowlist",
                        "created_at",
                        "last_used_at",
                        "revoked_at",
                        "expires_at",
                        "replaced_by",
                        "status"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "key_prefix": {
                          "type": "string",
                          "description": "First 16 characters of the issued token."
                        },
                        "dataset_default": {
                          "type": "string"
                        },
                        "require_signature": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "ip_allowlist": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The allowlist as stored: a JSON array of IPs/CIDRs encoded as a string, or null for any address."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "last_used_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "revoked_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "expires_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "When the key stops authenticating; null means never."
                        },
                        "replaced_by": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "After a rotation, the id of the key that replaced this one."
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "expired",
                            "revoked"
                          ],
                          "description": "Only an active key authenticates."
                        }
                      },
                      "description": "The created ingest key, as GET by id returns it (#201)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Key name is required (at most 80 characters), dataset_default is not a valid dataset name, expires_at is not a future timestamp, or ip_allowlist is not an array of valid IPs/CIDRs (at most 64).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/ingest-keys. Write role required.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "dataset_default": {
                    "type": "string",
                    "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$",
                    "default": "default",
                    "description": "Where POST /v1/ingest/{ingest_key} (no dataset segment) stores an event none of the project's mapping rules routes: the whole payload (the identity mapping) as one record in this dataset, fanned out to destinations, the live stream, AI triggers and workflows like any other record, and charged the same single quota event. The 201 then answers routed: [\"<this dataset>\"], records: 1, fallback: true. A dataset named in the path (POST /v1/ingest/{ingest_key}/{dataset}) is written directly and endpoint URLs never use it. Must be a valid dataset name; a non-string value is stored as default. Listed with the key, carried over on rotation, and fixed at creation (#280)."
                  },
                  "require_signature": {
                    "type": "boolean",
                    "default": false,
                    "description": "Only the literal true enables it. When enabled a signing secret is generated and returned once."
                  },
                  "expires_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "When the key stops authenticating, at ingest and on /v1/stream alike. Must be in the future; null means never. A key past it answers 404 'Unknown ingest key', exactly like a revoked one."
                  },
                  "ip_allowlist": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "maxItems": 64,
                    "items": {
                      "type": "string",
                      "description": "An IPv4 or IPv6 address, or a CIDR range."
                    },
                    "description": "IP addresses or CIDR ranges allowed to use this credential, validated exactly as the workspace and project allowlists are. [] or null removes the restriction (stored as null). A request must pass every allowlist that is set: this one, the project's and the workspace's. Refused requests get 403 'Source IP not allowed' and an ingest_ip_blocked audit row.",
                    "example": [
                      "203.0.113.0/24",
                      "2001:db8::/32"
                    ]
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/ingest-keys/{id}": {
      "get": {
        "operationId": "getIngestKey",
        "summary": "Get one ingest key",
        "tags": [
          "Ingest keys"
        ],
        "description": "The object, as one row of the list returns it, or 404. This used to answer 200 with the whole list for any id (DX-6, #201). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ingest key id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ingest_key"
                  ],
                  "properties": {
                    "ingest_key": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "key_prefix",
                        "dataset_default",
                        "require_signature",
                        "ip_allowlist",
                        "created_at",
                        "last_used_at",
                        "revoked_at",
                        "expires_at",
                        "replaced_by",
                        "status"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "key_prefix": {
                          "type": "string",
                          "description": "First 16 characters of the issued token."
                        },
                        "dataset_default": {
                          "type": "string"
                        },
                        "require_signature": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "ip_allowlist": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The allowlist as stored: a JSON array of IPs/CIDRs encoded as a string, or null for any address."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "last_used_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "revoked_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "expires_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "When the key stops authenticating; null means never."
                        },
                        "replaced_by": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "After a rotation, the id of the key that replaced this one."
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "expired",
                            "revoked"
                          ],
                          "description": "Only an active key authenticates."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Key not found - unknown id, another project, or already revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "revokeIngestKey",
        "summary": "Revoke an ingest key",
        "tags": [
          "Ingest keys"
        ],
        "responses": {
          "200": {
            "description": "Revoked. The row is kept with revoked_at stamped, not deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Key not found - unknown id, another project, or already revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: DELETE /admin/api/ingest-keys/{id}. Write role required. The key stops authenticating on the next request, at ingest and on /v1/stream. To replace a key without an outage, rotate it instead.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ingest key id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "operationId": "updateIngestKey",
        "summary": "Rename an ingest key, or set its expiry or IP allowlist",
        "tags": [
          "Ingest keys"
        ],
        "description": "Top-level alias: PATCH /admin/api/ingest-keys/{id}. Write role required. Send at least one of name, expires_at and ip_allowlist. dataset_default and require_signature are fixed at creation. A revoked or expired key cannot be changed (409) - nothing revives a dead credential. On a rotated key, moving expires_at moves the end of the overlap. Audited as update_ingest_key with each change's from and to.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ingest key id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "expires_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "When the key stops authenticating, at ingest and on /v1/stream alike. Must be in the future; null means never. A key past it answers 404 'Unknown ingest key', exactly like a revoked one."
                  },
                  "ip_allowlist": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "maxItems": 64,
                    "items": {
                      "type": "string",
                      "description": "An IPv4 or IPv6 address, or a CIDR range."
                    },
                    "description": "IP addresses or CIDR ranges allowed to use this credential, validated exactly as the workspace and project allowlists are. [] or null removes the restriction (stored as null). A request must pass every allowlist that is set: this one, the project's and the workspace's. Refused requests get 403 'Source IP not allowed' and an ingest_ip_blocked audit row.",
                    "example": [
                      "203.0.113.0/24",
                      "2001:db8::/32"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated. Echoes what changed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "name": {
                      "type": "string"
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "ip_allowlist": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Body is not an object, sends no field, sends a field that cannot change (dataset_default, require_signature, ...), or a field is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Key not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The key is revoked or expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/destinations": {
      "get": {
        "operationId": "listDestinations",
        "summary": "List outbound destinations",
        "tags": [
          "Destinations"
        ],
        "responses": {
          "200": {
            "description": "Destinations of the project, newest first. Neither signing secret is included here, nor any secret header value or authentication credential (DLV-7): a secret header is listed by name only and auth by its type. dataset_filter is the stored JSON string (or null for every dataset). timeout_seconds, max_per_second and max_concurrency are null when the destination uses the default. type says how each destination delivers (webhook, slack, s3, sqs or pubsub) and config holds its settings without any credential (DLV-13).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "destinations"
                  ],
                  "properties": {
                    "destinations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "url",
                          "dataset_filter",
                          "enabled",
                          "created_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri",
                            "description": "Where deliveries go. For a webhook, the https URL it POSTs to. For Slack, the masked webhook URL; for S3, the bucket's base URL (with the prefix): display values, safe to show, never a credential."
                          },
                          "dataset_filter": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "JSON array of dataset names, or null for all datasets."
                          },
                          "enabled": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "timeout_seconds": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "minimum": 1,
                            "maximum": 30,
                            "description": "Seconds to wait for the receiver's answer before the attempt counts as failed (1-30). null: the default, 10."
                          },
                          "max_per_second": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "minimum": 0.1,
                            "maximum": 1000,
                            "description": "Most deliveries a second to this destination (0.1-1000). null: no limit. A delivery over it waits on the queue for its reserved slot; it is not failed and is not counted as an attempt."
                          },
                          "max_concurrency": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "minimum": 1,
                            "maximum": 100,
                            "description": "Most requests in flight to this destination at once (1-100). null: no cap. A delivery over it waits on the queue; it is not failed and is not counted as an attempt."
                          },
                          "previous_secret_expires_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "While a rotated-out signing secret still signs (for 24 hours after a rotation): when it stops. null outside that window. The secret itself is never returned."
                          },
                          "portal_id": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Set on a destination a portal customer created (#198, PORT-7); null on the workspace's own."
                          },
                          "customer_id": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The portal customer it belongs to (the workspace's own identifier)."
                          },
                          "customer_name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "That customer's display name, as the most recent token that gave one names them."
                          },
                          "paused_reason": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "over_plan_limit",
                              null
                            ],
                            "description": "Why the row is off when its user did not turn it off (#212). `over_plan_limit`: the workspace's plan dropped below what it runs, so the newest rows beyond the new plan's cap were paused (enabled 0); they resume by themselves, oldest first, when the plan allows them again. null for a running row or one a user disabled, which no plan change ever turns back on. Toggling `enabled` clears it."
                          },
                          "headers": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "name",
                                "secret"
                              ],
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "value": {
                                  "type": "string",
                                  "description": "Absent when secret."
                                },
                                "secret": {
                                  "type": "boolean"
                                }
                              }
                            },
                            "description": "Custom headers. A secret one has no value here, ever."
                          },
                          "auth": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "required": [
                              "type"
                            ],
                            "properties": {
                              "type": {
                                "type": "string",
                                "enum": [
                                  "bearer",
                                  "basic",
                                  "api_key"
                                ]
                              },
                              "header": {
                                "type": "string",
                                "description": "api_key only: the header the key goes in."
                              }
                            },
                            "description": "The authentication type only; the credential is never returned."
                          },
                          "transform": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "The payload transformation as saved ({type:'template', template} or {type:'mapping', mappings}), or null for the envelope."
                          },
                          "alert_url": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "uri"
                          },
                          "failing": {
                            "type": "boolean",
                            "description": "OBS-2: a delivery to it went dead and none has arrived since, or Hookie switched it off."
                          },
                          "consecutive_dead": {
                            "type": "integer",
                            "description": "Deliveries in a row that went dead (retries exhausted, or refused outright) since the last delivered one. Retrying deliveries do not count."
                          },
                          "failing_since": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "When the current streak's first dead delivery died."
                          },
                          "last_failure": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The error of the latest dead delivery in the streak."
                          },
                          "auto_disabled_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "Set when Hookie switched the destination off: 10 dead deliveries in a row over at least an hour. New deliveries are then held (paused), and PATCH enabled: true sends them."
                          },
                          "disabled_reason": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Why Hookie switched it off, in words for a person."
                          },
                          "type": {
                            "$ref": "#/components/schemas/DestinationType"
                          },
                          "config": {
                            "$ref": "#/components/schemas/DestinationConfig"
                          },
                          "secrets_set": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "webhook_url",
                                "secret_access_key",
                                "service_account_json"
                              ]
                            },
                            "description": "Slack, S3, SQS and Pub/Sub only: which credentials are stored (their values never leave the server)."
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/destinations. Readable by any role. Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "operationId": "createDestination",
        "summary": "Create an outbound destination",
        "tags": [
          "Destinations"
        ],
        "responses": {
          "201": {
            "description": "Created. signing_secret is returned here in plaintext; it is stored AES-256-GCM encrypted and can be read back later via the secret endpoint. Every type gets a signing secret: a webhook's deliveries are signed with it, and for Slack, S3, SQS and Pub/Sub it signs only the destination's alert_url notices.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "signing_secret"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "signing_secret": {
                      "type": "string",
                      "description": "whsec_ followed by 48 hex characters."
                    },
                    "warning": {
                      "type": "string",
                      "description": "Present when the destination's URL is one of this project's own endpoint URLs or ingest keys and its dataset filter admits the dataset that writes to, so every event would loop back (stopped after 8 hops by Hookie-Hop). The destination is still created."
                    },
                    "destination": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "url",
                        "dataset_filter",
                        "enabled",
                        "created_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "description": "Where deliveries go. For a webhook, the https URL it POSTs to. For Slack, the masked webhook URL; for S3, the bucket's base URL (with the prefix): display values, safe to show, never a credential."
                        },
                        "dataset_filter": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "JSON array of dataset names, or null for all datasets."
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "timeout_seconds": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "minimum": 1,
                          "maximum": 30,
                          "description": "Seconds to wait for the receiver's answer before the attempt counts as failed (1-30). null: the default, 10."
                        },
                        "max_per_second": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "minimum": 0.1,
                          "maximum": 1000,
                          "description": "Most deliveries a second to this destination (0.1-1000). null: no limit. A delivery over it waits on the queue for its reserved slot; it is not failed and is not counted as an attempt."
                        },
                        "max_concurrency": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "minimum": 1,
                          "maximum": 100,
                          "description": "Most requests in flight to this destination at once (1-100). null: no cap. A delivery over it waits on the queue; it is not failed and is not counted as an attempt."
                        },
                        "previous_secret_expires_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "While a rotated-out signing secret still signs (for 24 hours after a rotation): when it stops. null outside that window. The secret itself is never returned."
                        },
                        "type": {
                          "$ref": "#/components/schemas/DestinationType"
                        },
                        "config": {
                          "$ref": "#/components/schemas/DestinationConfig"
                        },
                        "secrets_set": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "webhook_url",
                              "secret_access_key",
                              "service_account_json"
                            ]
                          },
                          "description": "Slack, S3, SQS and Pub/Sub only: which credentials are stored (their values never leave the server)."
                        }
                      },
                      "description": "The created destination, as GET by id returns it (#201)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failure - name missing or over 80 characters, url missing, not absolute, or not https, an invalid datasetFilter entry, a reserved, malformed or duplicated header, a header value with a line break, an unknown auth type or a missing credential, a custom header that is also the authentication's header, an invalid transform (unknown type, bad mapping key, over 16 KB), or an alert_url that is not a public https URL, an unknown type, a url, headers or auth on a Slack, S3, SQS or Pub/Sub destination, config on a webhook, or a typed config that is missing or invalid (a webhook_url not on https://hooks.slack.com/, an endpoint that is not a public https origin, a bad region, bucket, prefix or access key, a missing secret_access_key, a queue_url that is not on an SQS endpoint or a region that does not match it, a bad project_id or topic, or a service_account_json that is not JSON, has no service account's client_email or usable private_key, or names a token_uri other than https://oauth2.googleapis.com/token).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Plan limit reached (free 1, standard 10, team 25 destinations per workspace) - upgrade to add more.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/destinations. Write role required. The workspace-wide cap counts destinations with a non-null project_id and is checked before validation.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "webhook only, and required for it: an absolute https URL. http and relative URLs are rejected. Normalized through the URL parser before storing. Refused for Slack, S3, SQS and Pub/Sub, whose address is their config. A Slack incoming-webhook URL (hooks.slack.com) is refused here (400): it is a credential, stored encrypted only as a `slack` destination's config.webhook_url (#313)."
                  },
                  "datasetFilter": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string",
                      "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$"
                    },
                    "description": "CAMELCASE on create - the create validator reads datasetFilter, while PATCH reads dataset_filter. Omitted or null means every dataset."
                  },
                  "enabled": {
                    "type": "boolean",
                    "default": true,
                    "description": "Anything other than the literal false is treated as true."
                  },
                  "headers": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "maxItems": 20,
                    "items": {
                      "type": "object",
                      "required": [
                        "name"
                      ],
                      "properties": {
                        "name": {
                          "type": "string",
                          "maxLength": 100,
                          "description": "An HTTP header name (RFC 9110 token). Reserved and refused: Hookie-*, X-Hookie-*, webhook-*, Host, Content-Length, Content-Type, Idempotency-Key, User-Agent, and hop-by-hop headers (Connection, Keep-Alive, TE, Trailer, Transfer-Encoding, Upgrade, Proxy-*). Names are unique, case-insensitively."
                        },
                        "value": {
                          "type": "string",
                          "maxLength": 4096,
                          "description": "No CR, LF or other control characters. On PATCH, a secret header sent WITHOUT a value keeps the value already stored for that name."
                        },
                        "secret": {
                          "type": "boolean",
                          "default": false,
                          "description": "Store the value AES-256-GCM encrypted and never return it."
                        }
                      },
                      "additionalProperties": false
                    },
                    "description": "Custom headers sent with every delivery (DLV-7). Replaces the whole list; null clears it. Sent before Hookie's own headers, which always win. webhook only: refused for Slack, S3, SQS and Pub/Sub."
                  },
                  "auth": {
                    "description": "Authentication (DLV-7), stored AES-256-GCM encrypted and never returned. null or {type:'none'} removes it. On PATCH, the same type with no credential keeps the stored credential (so an API key's header can be renamed without re-entering it). webhook only: refused for Slack, S3, SQS and Pub/Sub (S3 and SQS are authenticated by SigV4 from config, Pub/Sub by its service-account key).",
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "required": [
                          "type"
                        ],
                        "properties": {
                          "type": {
                            "const": "none"
                          }
                        },
                        "additionalProperties": false
                      },
                      {
                        "type": "object",
                        "required": [
                          "type"
                        ],
                        "properties": {
                          "type": {
                            "const": "bearer"
                          },
                          "token": {
                            "type": "string",
                            "maxLength": 4096
                          }
                        },
                        "additionalProperties": false,
                        "description": "Sent as Authorization: Bearer <token>."
                      },
                      {
                        "type": "object",
                        "required": [
                          "type"
                        ],
                        "properties": {
                          "type": {
                            "const": "basic"
                          },
                          "username": {
                            "type": "string",
                            "description": "May not contain ':'."
                          },
                          "password": {
                            "type": "string"
                          }
                        },
                        "additionalProperties": false,
                        "description": "Sent as Authorization: Basic base64(username:password), UTF-8."
                      },
                      {
                        "type": "object",
                        "required": [
                          "type",
                          "header"
                        ],
                        "properties": {
                          "type": {
                            "const": "api_key"
                          },
                          "header": {
                            "type": "string",
                            "description": "The header the key goes in, e.g. DD-API-KEY or X-Api-Key. Not a reserved header."
                          },
                          "value": {
                            "type": "string",
                            "maxLength": 4096
                          }
                        },
                        "additionalProperties": false
                      }
                    ]
                  },
                  "transform": {
                    "description": "Payload transformation (DLV-7): data, never code. null sends Hookie's envelope {id, dataset, received_at, data}. A template is any JSON value whose string leaves may hold {{path}} placeholders, read from the envelope with the mapping engine's dotted-path reader ({{dataset}}, {{data.customer.email}}, {{$payload}} for the whole envelope); a string that is exactly one placeholder keeps the value's type, an embedded one becomes text (objects as JSON, missing as empty). A mapping builds a flat object, one key per {path, key}, as routing rules do ($submission.id and $submission.received_at are the record's id and time). At most 16 KB serialised. The body is always sent as application/json and Hookie-Signature is computed over it as sent.",
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "required": [
                          "type",
                          "template"
                        ],
                        "properties": {
                          "type": {
                            "const": "template"
                          },
                          "template": {
                            "description": "Any JSON value, nested at most 20 levels."
                          }
                        },
                        "additionalProperties": false
                      },
                      {
                        "type": "object",
                        "required": [
                          "type",
                          "mappings"
                        ],
                        "properties": {
                          "type": {
                            "const": "mapping"
                          },
                          "mappings": {
                            "type": "array",
                            "minItems": 1,
                            "maxItems": 100,
                            "items": {
                              "type": "object",
                              "required": [
                                "path",
                                "key"
                              ],
                              "properties": {
                                "path": {
                                  "type": "string",
                                  "maxLength": 200
                                },
                                "key": {
                                  "type": "string",
                                  "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$"
                                }
                              },
                              "additionalProperties": false
                            }
                          }
                        },
                        "additionalProperties": false
                      }
                    ]
                  },
                  "alert_url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uri",
                    "description": "OBS-2: a public https URL (no credentials, no private or internal host) that receives one signed POST when Hookie switches this destination off for failing. Body: {type:'destination.disabled', occurred_at, destination:{id,name,url,project_id}, reason, consecutive_dead, failing_since, last_failure}; headers Hookie-Signature (the destination's signing secret, same scheme as a delivery) and Hookie-Alert: destination.disabled. Sent once, never retried, redirects not followed. null or empty removes it."
                  },
                  "type": {
                    "$ref": "#/components/schemas/DestinationType"
                  },
                  "config": {
                    "$ref": "#/components/schemas/DestinationConfigInput"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/destinations/{id}": {
      "get": {
        "operationId": "getDestination",
        "summary": "Get one destination",
        "tags": [
          "Destinations"
        ],
        "description": "The object, as one row of the list returns it, or 404. This used to answer 200 with the whole list for any id (DX-6, #201). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "destination"
                  ],
                  "properties": {
                    "destination": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "url",
                        "dataset_filter",
                        "enabled",
                        "created_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "description": "Where deliveries go. For a webhook, the https URL it POSTs to. For Slack, the masked webhook URL; for S3, the bucket's base URL (with the prefix): display values, safe to show, never a credential."
                        },
                        "dataset_filter": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "JSON array of dataset names, or null for all datasets."
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "timeout_seconds": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "minimum": 1,
                          "maximum": 30,
                          "description": "Seconds to wait for the receiver's answer before the attempt counts as failed (1-30). null: the default, 10."
                        },
                        "max_per_second": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "minimum": 0.1,
                          "maximum": 1000,
                          "description": "Most deliveries a second to this destination (0.1-1000). null: no limit. A delivery over it waits on the queue for its reserved slot; it is not failed and is not counted as an attempt."
                        },
                        "max_concurrency": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "minimum": 1,
                          "maximum": 100,
                          "description": "Most requests in flight to this destination at once (1-100). null: no cap. A delivery over it waits on the queue; it is not failed and is not counted as an attempt."
                        },
                        "previous_secret_expires_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "While a rotated-out signing secret still signs (for 24 hours after a rotation): when it stops. null outside that window. The secret itself is never returned."
                        },
                        "type": {
                          "$ref": "#/components/schemas/DestinationType"
                        },
                        "config": {
                          "$ref": "#/components/schemas/DestinationConfig"
                        },
                        "secrets_set": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "webhook_url",
                              "secret_access_key",
                              "service_account_json"
                            ]
                          },
                          "description": "Slack, S3, SQS and Pub/Sub only: which credentials are stored (their values never leave the server)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Destination not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateDestination",
        "summary": "Edit a destination: name, URL or type settings, on/off, dataset filter, request options, alert URL or delivery limits",
        "tags": [
          "Destinations"
        ],
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "resumed": {
                      "type": "integer",
                      "description": "Present when enabling sent deliveries the destination held while disabled."
                    },
                    "warning": {
                      "type": "string",
                      "description": "When a new url points back at this workspace's own ingest - legal, but usually a loop."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A name or url that create would refuse (blank name, non-https url), enabled must be a boolean, dataset_filter must be an array of dataset names or null, an invalid dataset name, more than 50 datasets, a delivery limit out of its bounds (timeout_seconds 1-30 whole, max_per_second 0.1-1000, max_concurrency 1-100 whole), a reserved, malformed or duplicated header, a secret header with no value and none stored, an invalid auth or transform, a custom header that is also the authentication's header, an alert_url that is not a public https URL, a type other than the destination's own, url, headers or auth on a Slack, S3, SQS or Pub/Sub destination, config on a webhook, an invalid Slack, S3, SQS or Pub/Sub config, or Nothing to update when no field was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Destination not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The destination belongs to a customer portal - its dataset filter is fixed by the portal event types, and its URL, headers, authentication and payload are the customer's. Only raised when dataset_filter, url, headers, auth or transform is in the body. Also `portal_customer_mismatch` (PORT-2g, #321): `enabled: true` on a portal customer's destination while the portal now serves a different customer (the body names them in `customer_id` / `customer_name`); switching it on would send that customer's events to this one. Nothing in the request is applied. Offboard the current customer first, or leave the destination off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "402": {
            "description": "`Plan limit reached` - enabled: true while the workspace already runs as many destinations as its plan allows (only possible after a downgrade paused some, #212). Upgrade, or turn another off first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Top-level alias: PATCH /admin/api/destinations/{id}. Write role required.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "description": "At least one of name, url, config, enabled, dataset_filter, timeout_seconds, max_per_second, max_concurrency, headers, auth, transform or alert_url. null on a limit restores its default. name and url are validated as on create (url must be https). The signing secret does not change with the URL - the signature covers the timestamp and body, not the address; rotate it on its own route. enabled: true also clears a failing destination's streak and any automatic disable (OBS-2), and sends the deliveries it held (the response's `resumed`). A destination's type cannot change (400 when type names another). Slack, S3, SQS and Pub/Sub destinations change their address through config, never url, and take no headers or auth.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https only. Refused (409) for a destination that belongs to a customer portal - its URL is the customer's. A Slack incoming-webhook URL (hooks.slack.com) is refused here (400): it is a credential, stored encrypted only as a `slack` destination's config.webhook_url (#313)."
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "dataset_filter": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string",
                      "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$"
                    },
                    "maxItems": 50,
                    "description": "SNAKE_CASE on patch (create uses datasetFilter). null, or a list that dedupes to empty, means every dataset."
                  },
                  "timeout_seconds": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 30,
                    "description": "Seconds to wait for the receiver's answer before the attempt counts as failed (1-30). null: the default, 10."
                  },
                  "max_per_second": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0.1,
                    "maximum": 1000,
                    "description": "Most deliveries a second to this destination (0.1-1000). null: no limit. A delivery over it waits on the queue for its reserved slot; it is not failed and is not counted as an attempt."
                  },
                  "max_concurrency": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 100,
                    "description": "Most requests in flight to this destination at once (1-100). null: no cap. A delivery over it waits on the queue; it is not failed and is not counted as an attempt."
                  },
                  "headers": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "maxItems": 20,
                    "items": {
                      "type": "object",
                      "required": [
                        "name"
                      ],
                      "properties": {
                        "name": {
                          "type": "string",
                          "maxLength": 100,
                          "description": "An HTTP header name (RFC 9110 token). Reserved and refused: Hookie-*, X-Hookie-*, webhook-*, Host, Content-Length, Content-Type, Idempotency-Key, User-Agent, and hop-by-hop headers (Connection, Keep-Alive, TE, Trailer, Transfer-Encoding, Upgrade, Proxy-*). Names are unique, case-insensitively."
                        },
                        "value": {
                          "type": "string",
                          "maxLength": 4096,
                          "description": "No CR, LF or other control characters. On PATCH, a secret header sent WITHOUT a value keeps the value already stored for that name."
                        },
                        "secret": {
                          "type": "boolean",
                          "default": false,
                          "description": "Store the value AES-256-GCM encrypted and never return it."
                        }
                      },
                      "additionalProperties": false
                    },
                    "description": "Custom headers sent with every delivery (DLV-7). Replaces the whole list; null clears it. Sent before Hookie's own headers, which always win."
                  },
                  "auth": {
                    "description": "Authentication (DLV-7), stored AES-256-GCM encrypted and never returned. null or {type:'none'} removes it. On PATCH, the same type with no credential keeps the stored credential (so an API key's header can be renamed without re-entering it).",
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "required": [
                          "type"
                        ],
                        "properties": {
                          "type": {
                            "const": "none"
                          }
                        },
                        "additionalProperties": false
                      },
                      {
                        "type": "object",
                        "required": [
                          "type"
                        ],
                        "properties": {
                          "type": {
                            "const": "bearer"
                          },
                          "token": {
                            "type": "string",
                            "maxLength": 4096
                          }
                        },
                        "additionalProperties": false,
                        "description": "Sent as Authorization: Bearer <token>."
                      },
                      {
                        "type": "object",
                        "required": [
                          "type"
                        ],
                        "properties": {
                          "type": {
                            "const": "basic"
                          },
                          "username": {
                            "type": "string",
                            "description": "May not contain ':'."
                          },
                          "password": {
                            "type": "string"
                          }
                        },
                        "additionalProperties": false,
                        "description": "Sent as Authorization: Basic base64(username:password), UTF-8."
                      },
                      {
                        "type": "object",
                        "required": [
                          "type",
                          "header"
                        ],
                        "properties": {
                          "type": {
                            "const": "api_key"
                          },
                          "header": {
                            "type": "string",
                            "description": "The header the key goes in, e.g. DD-API-KEY or X-Api-Key. Not a reserved header."
                          },
                          "value": {
                            "type": "string",
                            "maxLength": 4096
                          }
                        },
                        "additionalProperties": false
                      }
                    ]
                  },
                  "transform": {
                    "description": "Payload transformation (DLV-7): data, never code. null sends Hookie's envelope {id, dataset, received_at, data}. A template is any JSON value whose string leaves may hold {{path}} placeholders, read from the envelope with the mapping engine's dotted-path reader ({{dataset}}, {{data.customer.email}}, {{$payload}} for the whole envelope); a string that is exactly one placeholder keeps the value's type, an embedded one becomes text (objects as JSON, missing as empty). A mapping builds a flat object, one key per {path, key}, as routing rules do ($submission.id and $submission.received_at are the record's id and time). At most 16 KB serialised. The body is always sent as application/json and Hookie-Signature is computed over it as sent.",
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "required": [
                          "type",
                          "template"
                        ],
                        "properties": {
                          "type": {
                            "const": "template"
                          },
                          "template": {
                            "description": "Any JSON value, nested at most 20 levels."
                          }
                        },
                        "additionalProperties": false
                      },
                      {
                        "type": "object",
                        "required": [
                          "type",
                          "mappings"
                        ],
                        "properties": {
                          "type": {
                            "const": "mapping"
                          },
                          "mappings": {
                            "type": "array",
                            "minItems": 1,
                            "maxItems": 100,
                            "items": {
                              "type": "object",
                              "required": [
                                "path",
                                "key"
                              ],
                              "properties": {
                                "path": {
                                  "type": "string",
                                  "maxLength": 200
                                },
                                "key": {
                                  "type": "string",
                                  "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$"
                                }
                              },
                              "additionalProperties": false
                            }
                          }
                        },
                        "additionalProperties": false
                      }
                    ]
                  },
                  "alert_url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uri",
                    "description": "OBS-2: a public https URL (no credentials, no private or internal host) that receives one signed POST when Hookie switches this destination off for failing. Body: {type:'destination.disabled', occurred_at, destination:{id,name,url,project_id}, reason, consecutive_dead, failing_since, last_failure}; headers Hookie-Signature (the destination's signing secret, same scheme as a delivery) and Hookie-Alert: destination.disabled. Sent once, never retried, redirects not followed. null or empty removes it."
                  },
                  "type": {
                    "$ref": "#/components/schemas/DestinationType",
                    "description": "Optional; only the destination's own type is accepted."
                  },
                  "config": {
                    "$ref": "#/components/schemas/DestinationConfigInput"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "delete": {
        "operationId": "deleteDestination",
        "summary": "Delete a destination",
        "tags": [
          "Destinations"
        ],
        "responses": {
          "200": {
            "description": "Deleted. A destination with no delivery history is removed outright. One with history cannot be (deliveries.destination_id is a NOT NULL foreign key), so it is severed instead - project_id NULL, enabled 0 - which takes it out of every project list, count and fan-out for good while each past delivery still records where it went.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Destination not found in this project (including one already detached).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: DELETE /admin/api/destinations/{id}. Write role required.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/destinations/{id}/secret": {
      "get": {
        "operationId": "getDestinationSecret",
        "summary": "Reveal a destination signing secret",
        "tags": [
          "Destinations"
        ],
        "responses": {
          "200": {
            "description": "The decrypted signing secret. Sent with Cache-Control: no-store, and every reveal is written to the audit log as reveal_destination_secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "signing_secret"
                  ],
                  "properties": {
                    "signing_secret": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role - owner or admin only (a developer is refused), and ALWAYS refused for an OAuth-connected agent whatever its scopes. An agent is refused with `code: \"agent_not_permitted\"`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Destination not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/destinations/{id}/secret. Any sub-path other than secret or rotate-secret returns 404 Not found.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/destinations/{id}/rotate-secret": {
      "post": {
        "operationId": "rotateDestinationSecret",
        "summary": "Rotate a destination signing secret",
        "tags": [
          "Destinations"
        ],
        "responses": {
          "200": {
            "description": "Rotated, returning the new plaintext secret (status 200, not 201). Sent with Cache-Control: no-store and audited as rotate_destination_secret. The secret it replaced keeps signing for 24 hours: until previous_secret_expires_at every delivery's Hookie-Signature carries two v1 values, the new secret's first (t=<unix>,v1=<new>,v1=<old>), and its Standard Webhooks webhook-signature two space-separated entries in the same order (v1,<new> v1,<old>), so the receiver can be moved to the new secret at any point in the window. Rotating again inside a window replaces the older secret. End the window early with expire-previous-secret. No request body is read.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "signing_secret",
                    "previous_secret_expires_at"
                  ],
                  "properties": {
                    "signing_secret": {
                      "type": "string",
                      "description": "whsec_ followed by 48 hex characters."
                    },
                    "previous_secret_expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the replaced secret stops signing: 24 hours from now."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role - owner or admin only, never an OAuth-connected agent; also returned for a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. An agent is refused with `code: \"agent_not_permitted\"`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Destination not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/destinations/{id}/rotate-secret.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/destinations/{id}/expire-previous-secret": {
      "post": {
        "operationId": "expireDestinationPreviousSecret",
        "summary": "Stop signing with a destination's previous secret",
        "tags": [
          "Destinations"
        ],
        "description": "Top-level alias: POST /admin/api/destinations/{id}/expire-previous-secret. Ends a rotation's 24-hour overlap early: from the next delivery only the current secret signs. Owner or admin only, never an OAuth-connected agent; audited as expire_destination_previous_secret. Succeeds (and does nothing) when no previous secret is live. No request body is read.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The previous secret is gone.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role - owner or admin only, never an OAuth-connected agent; also returned for a cookie-authenticated mutation missing the X-Requested-With: fetch header. An agent is refused with `code: \"agent_not_permitted\"`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Destination not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/deliveries": {
      "get": {
        "operationId": "listDeliveries",
        "summary": "List outbound deliveries",
        "tags": [
          "Deliveries"
        ],
        "responses": {
          "200": {
            "description": "The project's deliveries, newest first, joined to their destination for name and URL, optionally filtered by status and destination. With `limit`/`offset` the list is paged and `total` counts every match; without them it is the 500 newest. The stored response body is deliberately omitted here - fetch one delivery for it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deliveries"
                  ],
                  "properties": {
                    "deliveries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "destination_id",
                          "destination_name",
                          "destination_url",
                          "event_id",
                          "dataset",
                          "status",
                          "response_code",
                          "response_ms",
                          "attempts",
                          "max_attempts",
                          "next_retry_at",
                          "error",
                          "created_at",
                          "updated_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "destination_id": {
                            "type": "string"
                          },
                          "destination_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "destination_url": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "event_id": {
                            "type": "string",
                            "description": "The record id; a replay carries a #replay: suffix."
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "queued",
                              "delivering",
                              "delivered",
                              "failed",
                              "dead",
                              "paused",
                              "cancelled"
                            ]
                          },
                          "response_code": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "response_ms": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "attempts": {
                            "type": "integer"
                          },
                          "max_attempts": {
                            "type": "integer"
                          },
                          "next_retry_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Deliveries matching the filters, across all pages."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "An unknown `status`. Also: since or until is not an ISO 8601 timestamp.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/deliveries. Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only these statuses: one or more of queued, delivered, failed, dead, comma-separated. Anything else is a 400.",
            "schema": {
              "type": "string",
              "example": "failed,dead"
            }
          },
          {
            "name": "destination_id",
            "in": "query",
            "required": false,
            "description": "Only deliveries to this destination.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Only deliveries created at or after this ISO 8601 time. Anything that is not a timestamp is a 400.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "Only deliveries created at or before this ISO 8601 time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size (default 25, max 100). Giving limit or offset turns paging on; without either the answer is the 500 newest, as before.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, newest first.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/deliveries/metrics": {
      "get": {
        "operationId": "getDeliveryMetrics",
        "summary": "Delivery health per destination: success rate, latency percentiles, error classes, time series",
        "tags": [
          "Deliveries"
        ],
        "description": "Every attempt in the window, the failed ones and the successful ones, per destination in the project and in total. Answers which destination is failing, whether it refuses requests (4xx) or fails to serve them (5xx, timeouts), and how slow it is. The series is hourly for 24h and 7d, daily for 30d, with every bucket present.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "window",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "30d"
              ],
              "default": "24h"
            }
          },
          {
            "name": "destination_id",
            "in": "query",
            "description": "Only this destination.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delivery health for the window.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "window",
                    "since",
                    "until",
                    "destination_id",
                    "bucket",
                    "bucket_ms",
                    "totals",
                    "destinations",
                    "series"
                  ],
                  "properties": {
                    "window": {
                      "type": "string",
                      "enum": [
                        "24h",
                        "7d",
                        "30d"
                      ]
                    },
                    "since": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "until": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "destination_id": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "bucket": {
                      "type": "string",
                      "enum": [
                        "hour",
                        "day"
                      ]
                    },
                    "bucket_ms": {
                      "type": "integer"
                    },
                    "totals": {
                      "type": "object",
                      "required": [
                        "deliveries",
                        "success_rate",
                        "attempts",
                        "attempts_ok",
                        "attempt_success_rate",
                        "latency_ms",
                        "errors"
                      ],
                      "properties": {
                        "deliveries": {
                          "type": "object",
                          "required": [
                            "total",
                            "delivered",
                            "failed",
                            "pending"
                          ],
                          "description": "Deliveries created in the window, by where they ended up.",
                          "properties": {
                            "total": {
                              "type": "integer"
                            },
                            "delivered": {
                              "type": "integer"
                            },
                            "failed": {
                              "type": "integer",
                              "description": "Dead, or failed with no retry ahead of it."
                            },
                            "pending": {
                              "type": "integer",
                              "description": "Queued, paused, or failed with a retry scheduled."
                            }
                          }
                        },
                        "success_rate": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "description": "delivered / (delivered + failed). Null until a delivery has finished."
                        },
                        "attempts": {
                          "type": "integer",
                          "description": "Attempts made in the window."
                        },
                        "attempts_ok": {
                          "type": "integer",
                          "description": "Attempts that got a 2xx."
                        },
                        "attempt_success_rate": {
                          "type": [
                            "number",
                            "null"
                          ]
                        },
                        "latency_ms": {
                          "type": "object",
                          "required": [
                            "p50",
                            "p95",
                            "p99"
                          ],
                          "description": "Nearest-rank percentiles over every attempt's latency in the window, failed attempts included: a timeout counts as its timeout.",
                          "properties": {
                            "p50": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            },
                            "p95": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            },
                            "p99": {
                              "type": [
                                "integer",
                                "null"
                              ]
                            }
                          }
                        },
                        "errors": {
                          "type": "object",
                          "required": [
                            "3xx",
                            "4xx",
                            "5xx",
                            "timeout",
                            "connection",
                            "other"
                          ],
                          "description": "Failed attempts in the window by class. timeout: no answer within the destination's timeout; connection: refused, reset, DNS or TLS.",
                          "properties": {
                            "3xx": {
                              "type": "integer"
                            },
                            "4xx": {
                              "type": "integer"
                            },
                            "5xx": {
                              "type": "integer"
                            },
                            "timeout": {
                              "type": "integer"
                            },
                            "connection": {
                              "type": "integer"
                            },
                            "other": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    },
                    "destinations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "destination_id",
                          "destination_name",
                          "enabled",
                          "deliveries",
                          "success_rate",
                          "attempts",
                          "attempts_ok",
                          "attempt_success_rate",
                          "latency_ms",
                          "errors"
                        ],
                        "properties": {
                          "destination_id": {
                            "type": "string"
                          },
                          "destination_name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Null for a destination deleted since, whose history is still in the window."
                          },
                          "enabled": {
                            "type": "boolean"
                          },
                          "deliveries": {
                            "type": "object",
                            "required": [
                              "total",
                              "delivered",
                              "failed",
                              "pending"
                            ],
                            "description": "Deliveries created in the window, by where they ended up.",
                            "properties": {
                              "total": {
                                "type": "integer"
                              },
                              "delivered": {
                                "type": "integer"
                              },
                              "failed": {
                                "type": "integer",
                                "description": "Dead, or failed with no retry ahead of it."
                              },
                              "pending": {
                                "type": "integer",
                                "description": "Queued, paused, or failed with a retry scheduled."
                              }
                            }
                          },
                          "success_rate": {
                            "type": [
                              "number",
                              "null"
                            ],
                            "description": "delivered / (delivered + failed). Null until a delivery has finished."
                          },
                          "attempts": {
                            "type": "integer",
                            "description": "Attempts made in the window."
                          },
                          "attempts_ok": {
                            "type": "integer",
                            "description": "Attempts that got a 2xx."
                          },
                          "attempt_success_rate": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "latency_ms": {
                            "type": "object",
                            "required": [
                              "p50",
                              "p95",
                              "p99"
                            ],
                            "description": "Nearest-rank percentiles over every attempt's latency in the window, failed attempts included: a timeout counts as its timeout.",
                            "properties": {
                              "p50": {
                                "type": [
                                  "integer",
                                  "null"
                                ]
                              },
                              "p95": {
                                "type": [
                                  "integer",
                                  "null"
                                ]
                              },
                              "p99": {
                                "type": [
                                  "integer",
                                  "null"
                                ]
                              }
                            }
                          },
                          "errors": {
                            "type": "object",
                            "required": [
                              "3xx",
                              "4xx",
                              "5xx",
                              "timeout",
                              "connection",
                              "other"
                            ],
                            "description": "Failed attempts in the window by class. timeout: no answer within the destination's timeout; connection: refused, reset, DNS or TLS.",
                            "properties": {
                              "3xx": {
                                "type": "integer"
                              },
                              "4xx": {
                                "type": "integer"
                              },
                              "5xx": {
                                "type": "integer"
                              },
                              "timeout": {
                                "type": "integer"
                              },
                              "connection": {
                                "type": "integer"
                              },
                              "other": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      },
                      "description": "The project's destinations, busy or not, then any deleted one with history in the window."
                    },
                    "series": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "t",
                          "ok",
                          "failed"
                        ],
                        "properties": {
                          "t": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Start of the bucket (UTC)."
                          },
                          "ok": {
                            "type": "integer"
                          },
                          "failed": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "window is not 24h, 7d or 30d.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/deliveries/replay": {
      "post": {
        "operationId": "replayDeliveries",
        "summary": "Replay every delivery that did not arrive, by filter",
        "tags": [
          "Deliveries"
        ],
        "description": "Top-level alias: POST /admin/api/deliveries/replay. Write role required. The recovery after a receiver outage: replays every delivery in the project that matches the filter and did not arrive - 'dead', or 'failed' with no retry scheduled (one still being retried is left to its retry). Only the latest delivery of each event to each destination matches (an event's original and a failed replay of it are one event, sent once), and only while the event is stored; deliveries to a deleted destination, or to a customer-portal destination whose portal no longer exposes the dataset, never match. Oldest first. Bounded per call: queues up to limit and reports how many still match, so call again while more is true; a replayed delivery stops matching, so repeating (or racing) a call never sends one twice. Each replay is a new delivery (event_id <record>#replay:<n>) charged to the monthly delivery allowance, like a single replay: the call queues as many as the allowance grants and stops there. Audited once per call as replay_deliveries.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "destination_id": {
                    "type": "string",
                    "description": "Only deliveries to this destination."
                  },
                  "status": {
                    "description": "dead, failed, or both (the default). A list, or a comma-separated string. Any other status is a 400.",
                    "oneOf": [
                      {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "dead",
                            "failed"
                          ]
                        }
                      },
                      {
                        "type": "string"
                      }
                    ]
                  },
                  "since": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Only deliveries created at or after this time."
                  },
                  "until": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Only deliveries created at or before this time."
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 500,
                    "default": 100,
                    "description": "The most to queue in this call."
                  },
                  "dry_run": {
                    "type": "boolean",
                    "description": "Answer {matched, dry_run: true} without queueing or charging anything - what a confirmation states."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One bounded pass (or, with dry_run, the count alone).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "queued",
                        "matched",
                        "remaining",
                        "more"
                      ],
                      "properties": {
                        "queued": {
                          "type": "integer",
                          "description": "New deliveries queued by this call."
                        },
                        "matched": {
                          "type": "integer",
                          "description": "Deliveries matching the filter when the call began."
                        },
                        "remaining": {
                          "type": "integer",
                          "description": "Deliveries still matching after it."
                        },
                        "more": {
                          "type": "boolean",
                          "description": "remaining > 0: call again to continue."
                        },
                        "stopped": {
                          "type": "string",
                          "enum": [
                            "delivery_allowance_exhausted"
                          ],
                          "description": "Present when the allowance ran out part-way."
                        },
                        "error": {
                          "type": "string",
                          "description": "With stopped: the allowance message."
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "matched",
                        "dry_run"
                      ],
                      "properties": {
                        "matched": {
                          "type": "integer"
                        },
                        "dry_run": {
                          "type": "boolean",
                          "const": true
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "status names something other than dead or failed, since or until is not an ISO 8601 timestamp, destination_id is not a string, or limit is not a whole number from 1 to 500.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The month's delivery allowance is used up and nothing could be queued (reason: delivery_allowance_exhausted; the body also carries queued: 0, matched and remaining).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The replays could not be put on the delivery queue. Those not queued were withdrawn and not charged (queued says how many did go); try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/deliveries/held": {
      "get": {
        "operationId": "listHeldDeliveries",
        "summary": "List events whose deliveries are held past the monthly delivery allowance",
        "tags": [
          "Deliveries"
        ],
        "description": "Past the plan's delivery ceiling the fan-out stores the event as usual and HOLDS its remaining destinations: one hold per event, oldest first. Nothing is dropped; release them with POST .../deliveries/held/release once there is room.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Held events in this project, oldest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "holds",
                    "total",
                    "limit",
                    "offset",
                    "allowance"
                  ],
                  "properties": {
                    "holds": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "event_id",
                          "dataset",
                          "destinations",
                          "period",
                          "created_at",
                          "updated_at"
                        ],
                        "properties": {
                          "event_id": {
                            "type": "string",
                            "description": "The held event (a record id)."
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "destinations": {
                            "type": "integer",
                            "description": "How many destinations were held at the last attempt."
                          },
                          "period": {
                            "type": "string",
                            "description": "Billing period the hold began in, YYYY-MM."
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "allowance": {
                      "type": "object",
                      "required": [
                        "used",
                        "quota",
                        "ceiling"
                      ],
                      "properties": {
                        "used": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "Deliveries charged this month; null if the usage counter could not be read."
                        },
                        "quota": {
                          "type": "integer",
                          "description": "The plan's monthly delivery allowance."
                        },
                        "ceiling": {
                          "type": "integer",
                          "description": "The allowance plus the plan's soft overage: the point where deliveries are held."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/deliveries/held/release": {
      "post": {
        "operationId": "releaseHeldDeliveries",
        "summary": "Send held deliveries now that there is room in the allowance",
        "tags": [
          "Deliveries"
        ],
        "description": "Re-runs the fan-out for held events, oldest first, creating the deliveries each event's destinations still lack and charging them to the allowance. Stops at the first event it cannot fully cover (that event keeps a hold for what is left). A hold whose event has been pruned by retention is dropped. Writes one release_held_deliveries audit row.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 500,
                    "default": 100,
                    "description": "Most holds to process in this call."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What the release did.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "released",
                    "sent",
                    "dropped",
                    "remaining"
                  ],
                  "properties": {
                    "released": {
                      "type": "integer",
                      "description": "Holds fully released."
                    },
                    "sent": {
                      "type": "integer",
                      "description": "Deliveries created and queued."
                    },
                    "dropped": {
                      "type": "integer",
                      "description": "Holds removed because their event is gone."
                    },
                    "remaining": {
                      "type": "integer",
                      "description": "Holds still in this project."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/deliveries/{id}": {
      "get": {
        "operationId": "getDelivery",
        "summary": "Get one delivery with its response body and every attempt",
        "tags": [
          "Deliveries"
        ],
        "responses": {
          "200": {
            "description": "The delivery, including the captured response body (the only endpoint that returns it), and `attempts`: every attempt it made, oldest first. A failed attempt is kept as its own row; the successful one is the delivery itself.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "delivery",
                    "attempts",
                    "attempts_not_recorded"
                  ],
                  "properties": {
                    "delivery": {
                      "type": "object",
                      "required": [
                        "id",
                        "destination_id",
                        "destination_name",
                        "destination_url",
                        "event_id",
                        "dataset",
                        "status",
                        "response_code",
                        "response_ms",
                        "attempts",
                        "max_attempts",
                        "next_retry_at",
                        "error",
                        "response_body",
                        "response_content_type",
                        "created_at",
                        "updated_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "destination_id": {
                          "type": "string"
                        },
                        "destination_name": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "destination_url": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "event_id": {
                          "type": "string"
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "queued",
                            "delivering",
                            "delivered",
                            "failed",
                            "dead",
                            "paused",
                            "cancelled"
                          ]
                        },
                        "response_code": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "response_ms": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "attempts": {
                          "type": "integer"
                        },
                        "max_attempts": {
                          "type": "integer"
                        },
                        "next_retry_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "response_body": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Truncated at capture time (up to about 16 KiB)."
                        },
                        "response_content_type": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        }
                      }
                    },
                    "attempts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "n",
                          "at",
                          "ok",
                          "status_code",
                          "response_ms",
                          "error",
                          "response_body"
                        ],
                        "properties": {
                          "n": {
                            "type": "integer",
                            "description": "The attempt's number, from 1."
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the attempt settled."
                          },
                          "ok": {
                            "type": "boolean",
                            "description": "True for the attempt that got a 2xx (the delivery's last)."
                          },
                          "status_code": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "Null when nothing answered: a timeout, a refused connection, DNS or TLS."
                          },
                          "response_ms": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "response_body": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The first 512 characters of what the receiver answered. The delivery's own response_body keeps 16 KiB of the latest."
                          }
                        }
                      },
                      "description": "Every attempt, oldest first. Failed attempts are kept for the plan's retention window."
                    },
                    "attempts_not_recorded": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Attempts this delivery made before attempt history was kept (#194). 0 for any delivery created since."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Delivery not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/deliveries/{id}. Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Delivery id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/deliveries/{id}/replay": {
      "post": {
        "operationId": "replayDelivery",
        "summary": "Replay a delivery",
        "tags": [
          "Deliveries"
        ],
        "responses": {
          "201": {
            "description": "A new queued delivery was created for the same destination and event and pushed onto the outbound queue. Its event_id carries a #replay: suffix so it bypasses the idempotent delivery ledger. No request body is read.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Id of the new delivery."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Delivery not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The month's delivery allowance is used up (reason: delivery_allowance_exhausted). A replay is a new delivery and is charged; retries are not.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "Top-level alias: POST /admin/api/deliveries/{id}/replay. Write role required. Any other method or sub-path on a delivery returns 405 Method not allowed.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the delivery to replay.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/search/events": {
      "post": {
        "operationId": "searchEvents",
        "summary": "Search records (events)",
        "tags": [
          "Search"
        ],
        "responses": {
          "200": {
            "description": "One page of records, newest first, with the total the page came out of. payload is truncated to 2000 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "entity",
                    "count",
                    "total",
                    "limit",
                    "offset",
                    "results"
                  ],
                  "properties": {
                    "entity": {
                      "type": "string",
                      "const": "events"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "dataset",
                          "source",
                          "received_at",
                          "submission_id",
                          "payload"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "source": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "received_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "submission_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "payload": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "dataset: <validator message> when the dataset filter is not a valid identifier.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A cookie-authenticated request missing the X-Requested-With: fetch header (search is a POST, so the CSRF guard applies even though no role above read is needed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/search/events. No role above read is enforced. Any non-POST method returns 405.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "All filters optional and ANDed. An unparseable body is treated as {}.",
                "properties": {
                  "dataset": {
                    "type": "string",
                    "pattern": "^[A-Za-z][A-Za-z0-9_]{0,62}$"
                  },
                  "q": {
                    "type": "string",
                    "description": "Case-insensitive (ASCII) substring match against the raw payload JSON."
                  },
                  "since": {
                    "type": "string",
                    "description": "ISO timestamp; matches received_at >= since."
                  },
                  "until": {
                    "type": "string",
                    "description": "ISO timestamp; matches received_at <= until."
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200,
                    "default": 25
                  },
                  "offset": {
                    "type": "integer",
                    "minimum": 0,
                    "default": 0
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/search/deliveries": {
      "post": {
        "operationId": "searchDeliveries",
        "summary": "Search deliveries",
        "tags": [
          "Search"
        ],
        "responses": {
          "200": {
            "description": "One page of deliveries, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "entity",
                    "count",
                    "total",
                    "limit",
                    "offset",
                    "results"
                  ],
                  "properties": {
                    "entity": {
                      "type": "string",
                      "const": "deliveries"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "destination_id",
                          "event_id",
                          "dataset",
                          "status",
                          "response_code",
                          "response_ms",
                          "attempts",
                          "error",
                          "created_at",
                          "updated_at",
                          "destination_name",
                          "next_retry_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "destination_id": {
                            "type": "string"
                          },
                          "event_id": {
                            "type": "string"
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "response_code": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "response_ms": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "attempts": {
                            "type": "integer"
                          },
                          "error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "destination_name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The destination's name; null when the destination has been deleted."
                          },
                          "next_retry_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time",
                            "description": "When the next attempt is due while one is scheduled (a failed delivery that is retrying); null once it settles."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "response_code must be an integer, or min_latency_ms must be a number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A cookie-authenticated request missing the X-Requested-With: fetch header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/search/deliveries.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "All filters optional and ANDed. A blank response_code or min_latency_ms is ignored.",
                "properties": {
                  "status": {
                    "description": "One status, or an array of statuses matched with IN (\"failed\" and \"dead\" together are everything that did not arrive). Matched exactly.",
                    "oneOf": [
                      {
                        "type": "string",
                        "enum": [
                          "queued",
                          "delivering",
                          "delivered",
                          "failed",
                          "dead",
                          "paused",
                          "cancelled"
                        ]
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "queued",
                            "delivering",
                            "delivered",
                            "failed",
                            "dead",
                            "paused",
                            "cancelled"
                          ]
                        }
                      }
                    ]
                  },
                  "response_code": {
                    "type": "integer",
                    "description": "Must be an integer."
                  },
                  "destination_id": {
                    "type": "string"
                  },
                  "min_latency_ms": {
                    "type": "number",
                    "description": "Matches response_ms >= value."
                  },
                  "since": {
                    "type": "string",
                    "description": "ISO timestamp; matches created_at >= since."
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200,
                    "default": 25
                  },
                  "offset": {
                    "type": "integer",
                    "minimum": 0,
                    "default": 0
                  },
                  "until": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Upper bound on created_at (ISO 8601)."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/search/ai-runs": {
      "post": {
        "operationId": "searchAiRuns",
        "summary": "Search AI agent runs",
        "tags": [
          "Search"
        ],
        "responses": {
          "200": {
            "description": "One page of AI runs, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "entity",
                    "count",
                    "total",
                    "limit",
                    "offset",
                    "results"
                  ],
                  "properties": {
                    "entity": {
                      "type": "string",
                      "const": "ai_runs"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "trigger_id",
                          "event_id",
                          "status",
                          "model",
                          "input_tokens",
                          "output_tokens",
                          "error",
                          "created_at",
                          "finished_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "trigger_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "event_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string"
                          },
                          "model": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "input_tokens": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "output_tokens": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "finished_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A cookie-authenticated request missing the X-Requested-With: fetch header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/search/ai-runs.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "All filters optional and ANDed. Reads the agent_runs table.",
                "properties": {
                  "model": {
                    "type": "string"
                  },
                  "status": {
                    "description": "One status (queued, running, done, failed), or an array of them matched with IN.",
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    ]
                  },
                  "trigger_id": {
                    "type": "string"
                  },
                  "since": {
                    "type": "string",
                    "description": "ISO timestamp; matches created_at >= since."
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200,
                    "default": 25
                  },
                  "offset": {
                    "type": "integer",
                    "minimum": 0,
                    "default": 0
                  },
                  "until": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Upper bound on created_at (ISO 8601)."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/search/ai-calls": {
      "post": {
        "operationId": "searchAiCalls",
        "summary": "Search the AI call log",
        "tags": [
          "Search"
        ],
        "description": "Every AI call an AI trigger, workflow call_ai step or agent_call step made in this project, including calls refused for being over the monthly AI token allowance. Newest first. Lists carry a preview of the output; GET one call for the full texts. Top-level alias: POST /admin/api/search/ai-calls.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "All filters optional and ANDed.",
                "properties": {
                  "q": {
                    "type": "string",
                    "description": "Case-insensitive substring of the instructions, input or output."
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "ok",
                      "error",
                      "quota_exceeded"
                    ]
                  },
                  "source_kind": {
                    "type": "string",
                    "enum": [
                      "trigger",
                      "workflow",
                      "agent"
                    ]
                  },
                  "source_id": {
                    "type": "string"
                  },
                  "instance_id": {
                    "type": "string"
                  },
                  "model": {
                    "type": "string"
                  },
                  "since": {
                    "type": "string",
                    "description": "ISO 8601 timestamp; matches created_at >= since."
                  },
                  "until": {
                    "type": "string",
                    "description": "ISO 8601 timestamp; matches created_at <= until."
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200,
                    "default": 25
                  },
                  "offset": {
                    "type": "integer",
                    "minimum": 0,
                    "default": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One page of AI calls, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "entity",
                    "count",
                    "total",
                    "limit",
                    "offset",
                    "results"
                  ],
                  "properties": {
                    "entity": {
                      "type": "string",
                      "const": "ai_calls"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "created_at",
                          "error",
                          "id",
                          "input_tokens",
                          "input_truncated",
                          "instance_id",
                          "instructions_truncated",
                          "latency_ms",
                          "model",
                          "output_preview",
                          "output_tokens",
                          "output_truncated",
                          "source_id",
                          "source_kind",
                          "status",
                          "step",
                          "tokens_estimated"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "source_kind": {
                            "type": "string",
                            "enum": [
                              "trigger",
                              "workflow",
                              "agent"
                            ],
                            "description": "What made the call: an AI trigger, a workflow call_ai step, or an agent_call step."
                          },
                          "source_id": {
                            "type": "string",
                            "description": "The trigger, workflow or ai_agent id."
                          },
                          "instance_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "step": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Step path within the workflow, e.g. \"2\" or \"1.then[0]\"."
                          },
                          "model": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "ok",
                              "error",
                              "quota_exceeded"
                            ]
                          },
                          "error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "input_tokens": {
                            "type": "integer"
                          },
                          "output_tokens": {
                            "type": "integer"
                          },
                          "tokens_estimated": {
                            "type": "boolean",
                            "description": "True when the model reported no usage and the counts are estimated at about four characters a token."
                          },
                          "latency_ms": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "instructions_truncated": {
                            "type": "boolean"
                          },
                          "input_truncated": {
                            "type": "boolean"
                          },
                          "output_truncated": {
                            "type": "boolean"
                          },
                          "output_preview": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The first 200 characters of the output."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A cookie-authenticated request missing the X-Requested-With: fetch header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/search/ai-calls/{id}": {
      "get": {
        "operationId": "getAiCall",
        "summary": "Get one AI call in full",
        "tags": [
          "Search"
        ],
        "description": "The instructions, input and output of one AI call, as logged. Top-level alias: GET /admin/api/search/ai-calls/{id}.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "AI call id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ai_call"
                  ],
                  "properties": {
                    "ai_call": {
                      "type": "object",
                      "required": [
                        "created_at",
                        "error",
                        "id",
                        "input",
                        "input_tokens",
                        "input_truncated",
                        "instance_id",
                        "instructions",
                        "instructions_truncated",
                        "latency_ms",
                        "model",
                        "output",
                        "output_tokens",
                        "output_truncated",
                        "source_id",
                        "source_kind",
                        "status",
                        "step",
                        "tokens_estimated"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "source_kind": {
                          "type": "string",
                          "enum": [
                            "trigger",
                            "workflow",
                            "agent"
                          ],
                          "description": "What made the call: an AI trigger, a workflow call_ai step, or an agent_call step."
                        },
                        "source_id": {
                          "type": "string",
                          "description": "The trigger, workflow or ai_agent id."
                        },
                        "instance_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "step": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Step path within the workflow, e.g. \"2\" or \"1.then[0]\"."
                        },
                        "model": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "ok",
                            "error",
                            "quota_exceeded"
                          ]
                        },
                        "error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "input_tokens": {
                          "type": "integer"
                        },
                        "output_tokens": {
                          "type": "integer"
                        },
                        "tokens_estimated": {
                          "type": "boolean",
                          "description": "True when the model reported no usage and the counts are estimated at about four characters a token."
                        },
                        "latency_ms": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "instructions_truncated": {
                          "type": "boolean"
                        },
                        "input_truncated": {
                          "type": "boolean"
                        },
                        "output_truncated": {
                          "type": "boolean"
                        },
                        "instructions": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The instructions / system prompt, capped at 8,000 characters (instructions_truncated says when cut)."
                        },
                        "input": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The data the model was given, as JSON, capped at 8,000 characters."
                        },
                        "output": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "What the model answered, capped at 8,000 characters."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A cookie-authenticated request missing the X-Requested-With: fetch header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such AI call in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/search/submissions": {
      "post": {
        "operationId": "searchSubmissions",
        "summary": "Search raw submissions",
        "tags": [
          "Search"
        ],
        "responses": {
          "200": {
            "description": "One page of submissions, newest first. payload is truncated to 2000 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "entity",
                    "count",
                    "total",
                    "limit",
                    "offset",
                    "results"
                  ],
                  "properties": {
                    "entity": {
                      "type": "string",
                      "const": "submissions"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "received_at",
                          "source",
                          "forward_status",
                          "payload"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "received_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "source": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "forward_status": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "payload": {
                            "type": "string"
                          },
                          "endpoint_id": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "#193."
                          },
                          "request_method": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "ING-3 (#193). The full request, headers included, is on correlate."
                          },
                          "content_type": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "source_ip": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "dedup_source": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A cookie-authenticated request missing the X-Requested-With: fetch header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: POST /admin/api/search/submissions.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "All filters optional and ANDed.",
                "properties": {
                  "q": {
                    "type": "string",
                    "description": "Case-insensitive (ASCII) substring match against the raw submission payload."
                  },
                  "source": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "pending",
                      "sent",
                      "failed",
                      "filtered"
                    ],
                    "description": "Matched against forward_status."
                  },
                  "since": {
                    "type": "string",
                    "description": "ISO timestamp; matches received_at >= since."
                  },
                  "until": {
                    "type": "string",
                    "description": "ISO timestamp; matches received_at <= until."
                  },
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200,
                    "default": 25
                  },
                  "offset": {
                    "type": "integer",
                    "minimum": 0,
                    "default": 0
                  },
                  "endpoint_id": {
                    "type": "string",
                    "description": "Only submissions that arrived through this endpoint (webhook id) (#193)."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/search/correlate/{id}": {
      "get": {
        "operationId": "correlateById",
        "summary": "Correlate any pipeline id across the pipeline",
        "tags": [
          "Search"
        ],
        "responses": {
          "200": {
            "description": "The whole chain: the submission (or null when only records matched), every record produced from it with a preview of its payload, the deliveries (destination, status, error) and AI runs (with their error) keyed off those record ids, and the workflow runs those records started, each with its per-step status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "correlation_id",
                    "submission",
                    "records",
                    "deliveries",
                    "ai_runs",
                    "matched",
                    "workflow_runs"
                  ],
                  "properties": {
                    "correlation_id": {
                      "type": "string",
                      "description": "The resolved submission id."
                    },
                    "submission": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "received_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "source": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "forward_status": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "payload": {
                          "type": "string",
                          "description": "Truncated to 2000 characters."
                        },
                        "request": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "description": "The HTTP request the event arrived in (ING-3, #193). null for an event stored before #193, or one that did not arrive over HTTP (a polled database row, a cron or WebSocket trigger). Credentials are never stored: Authorization, Proxy-Authorization, Cookie and any header or query parameter named like a credential (key, api-key, token, secret, password, session, auth) is stored as \"[redacted]\"; Cloudflare and proxy hop headers (cf-*, x-forwarded-*, x-real-ip, true-client-ip, cdn-loop, host) are dropped.",
                          "required": [
                            "method",
                            "headers",
                            "query",
                            "content_type",
                            "source_ip"
                          ],
                          "properties": {
                            "method": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "example": "POST"
                            },
                            "headers": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "string"
                              },
                              "description": "Lowercased header names. At most 64 headers, each value cut at 1024 characters, about 8 KB in all.",
                              "example": {
                                "x-github-event": "push",
                                "user-agent": "GitHub-Hookshot/abc",
                                "authorization": "[redacted]"
                              }
                            },
                            "query": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "The query string without its '?', at most 2048 characters.",
                              "example": "topic=orders"
                            },
                            "content_type": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "The Content-Type header as sent, parameters included."
                            },
                            "source_ip": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "CF-Connecting-IP."
                            }
                          }
                        },
                        "endpoint_id": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The endpoint (webhook id) it arrived through (#193)."
                        },
                        "dedup_source": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "idempotency-key",
                            "standard-webhooks",
                            "svix",
                            "github",
                            "shopify",
                            "gitlab",
                            "twilio",
                            "linear",
                            "twitch",
                            "atlassian",
                            "stripe",
                            "slack",
                            null
                          ],
                          "description": "What its dedup key came from, if it had one (ING-9, #193)."
                        }
                      }
                    },
                    "records": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "dataset",
                          "source",
                          "received_at",
                          "payload",
                          "payload_length",
                          "payload_truncated"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "source": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "received_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "payload": {
                            "type": "string",
                            "description": "The record's payload, truncated to 2000 characters."
                          },
                          "payload_length": {
                            "type": "integer"
                          },
                          "payload_truncated": {
                            "type": "boolean"
                          }
                        }
                      }
                    },
                    "deliveries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "destination_id",
                          "event_id",
                          "status",
                          "response_code",
                          "response_ms",
                          "created_at",
                          "destination_name",
                          "dataset",
                          "attempts",
                          "next_retry_at",
                          "error",
                          "updated_at"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "destination_id": {
                            "type": "string"
                          },
                          "event_id": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "response_code": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "response_ms": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "destination_name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The destination's name; null when it has been deleted."
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "attempts": {
                            "type": "integer"
                          },
                          "next_retry_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "updated_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          }
                        }
                      }
                    },
                    "ai_runs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "trigger_id",
                          "event_id",
                          "status",
                          "model",
                          "created_at",
                          "finished_at",
                          "error"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "trigger_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "event_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string"
                          },
                          "model": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "finished_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "matched": {
                      "type": "string",
                      "enum": [
                        "record",
                        "submission",
                        "delivery",
                        "ai_run",
                        "workflow_run"
                      ],
                      "description": "Which kind of id was looked up."
                    },
                    "workflow_runs": {
                      "type": "array",
                      "description": "Workflow runs these records started (a run's correlation_id is its entry record's id, answered as record_id), oldest first, at most 50 (#196, #194). Each carries its per-step status and the step log's details.",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "workflow_id",
                          "workflow_name",
                          "state",
                          "record_id",
                          "current_step_index",
                          "last_error",
                          "engine",
                          "run_attempt",
                          "created_at",
                          "updated_at",
                          "steps"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "workflow_id": {
                            "type": "string"
                          },
                          "workflow_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "running",
                              "waiting",
                              "completed",
                              "failed",
                              "cancelled",
                              "timed_out"
                            ]
                          },
                          "current_step_index": {
                            "type": "integer"
                          },
                          "last_error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "record_id": {
                            "type": "string",
                            "description": "The entry record that started the run (its correlation_id)."
                          },
                          "engine": {
                            "type": "string",
                            "enum": [
                              "cf",
                              "legacy"
                            ]
                          },
                          "run_attempt": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "updated_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "steps": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "index",
                                "path",
                                "type",
                                "status",
                                "started_at",
                                "finished_at",
                                "error",
                                "details"
                              ],
                              "properties": {
                                "index": {
                                  "type": "integer"
                                },
                                "path": {
                                  "type": "string",
                                  "description": "\"2\", or \"2.then[0]\" inside a branch."
                                },
                                "type": {
                                  "type": "string"
                                },
                                "status": {
                                  "type": "string",
                                  "enum": [
                                    "running",
                                    "waiting",
                                    "completed",
                                    "failed",
                                    "skipped"
                                  ]
                                },
                                "started_at": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "format": "date-time"
                                },
                                "finished_at": {
                                  "type": [
                                    "string",
                                    "null"
                                  ],
                                  "format": "date-time"
                                },
                                "error": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "details": {
                                  "type": [
                                    "object",
                                    "null"
                                  ],
                                  "description": "What the step log kept about the step's latest event (#196): the arm a branch took, the record an emit wrote, an AI call's model and tokens, and bounded, redacted samples of input and output."
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "correlation id required (the id path segment is missing).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No record, submission, delivery or AI run with that id in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed - correlate is GET only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Top-level alias: GET /admin/api/search/correlate/{id}. Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A record, submission, delivery, AI run or workflow run id. A record id is resolved to its submission; a delivery, AI run or workflow run id to the record it belongs to (a replay's `#replay:<n>` suffix is ignored) and then to its submission.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/audit": {
      "get": {
        "operationId": "listAudit",
        "summary": "Read the workspace audit log",
        "tags": [
          "Audit"
        ],
        "responses": {
          "200": {
            "description": "Newest first. JSON by default; with format=csv the body is a text/csv attachment (hookie-audit.csv) with the header at,actor_email,actor_sub,actor_agent,action,target,details.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "audit",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "audit": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "at",
                          "actor_sub",
                          "actor_email",
                          "actor_agent",
                          "action",
                          "target",
                          "details"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "actor_sub": {
                            "type": "string"
                          },
                          "actor_email": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "actor_agent": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "OAuth client id when the actor was a connected agent."
                          },
                          "action": {
                            "type": "string"
                          },
                          "target": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "details": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "JSON-encoded string."
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows matching the same filters, for the pager."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role - only returned for format=csv, which requires the manage capability (owner or admin). For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "description": "Workspace-level (tenant-wide), NOT project-scoped - there is no /projects/{project_id}/audit form. JSON reads are open to any role; the CSV export is owner/admin. The console's list and its Export CSV send the same actor, action, since and until filters, so an export holds exactly the rows the list shows (up to the 5000-row cap). Rows are kept for the longer of the plan's data retention and 365 days, then pruned; the database refuses to delete a row younger than 365 days, and never rewrites one.",
        "parameters": [
          {
            "name": "actor",
            "in": "query",
            "required": false,
            "description": "Substring matched against actor_email OR actor_sub.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "action",
            "in": "query",
            "required": false,
            "description": "Exact action name, e.g. create_webhook or reveal_destination_secret.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "target",
            "in": "query",
            "required": false,
            "description": "Exact target id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "ISO timestamp; matches at >= since.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "ISO timestamp; matches at <= until.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; non-numeric or non-positive falls back to 25, values above 100 are clamped. Ignored when format=csv (that path takes a single 5000-row page).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Row offset; forced to 0 when format=csv.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Set to csv for a CSV attachment instead of JSON. Requires the manage capability (owner/admin) and is itself audited as export_audit.",
            "schema": {
              "type": "string",
              "enum": [
                "csv"
              ]
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/audit/actions": {
      "get": {
        "operationId": "listAuditActions",
        "summary": "List the actions recorded in the workspace audit log",
        "tags": [
          "Audit"
        ],
        "description": "The distinct action names present in this workspace's audit log, sorted, for an action filter. Only actions that were actually recorded are listed, so every one matches at least one row of GET /admin/api/audit?action=. Workspace-level; open to any role, like the JSON list.",
        "responses": {
          "200": {
            "description": "Sorted, distinct action names.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "actions"
                  ],
                  "properties": {
                    "actions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "e.g. create_webhook, revoke_portal_token."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed: only GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/sources": {
      "get": {
        "operationId": "listSources",
        "summary": "List inbound source connectors in a project",
        "tags": [
          "Sources"
        ],
        "responses": {
          "200": {
            "description": "Sources, newest first, with the source types this deployment polls and the plan's shortest poll interval. The encrypted `config` column is never returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sources",
                    "types",
                    "min_poll_interval"
                  ],
                  "properties": {
                    "sources": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "description": "`postgres`. A source stored as `mysql` or `http` before those were withdrawn (#196) still lists; its poll fails with a last_error saying it is not supported."
                          },
                          "dataset": {
                            "type": "string"
                          },
                          "cursor_column": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "last_cursor": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "poll_interval": {
                            "type": "integer",
                            "description": "Seconds."
                          },
                          "enabled": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ],
                            "description": "Raw D1 integer, not a boolean."
                          },
                          "last_run_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "last_error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "paused_reason": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "over_plan_limit",
                              null
                            ],
                            "description": "Why the row is off when its user did not turn it off (#212). `over_plan_limit`: the workspace's plan dropped below what it runs, so the newest rows beyond the new plan's cap were paused (enabled 0); they resume by themselves, oldest first, when the plan allows them again. null for a running row or one a user disabled, which no plan change ever turns back on. Toggling `enabled` clears it."
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "types": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "postgres"
                        ]
                      },
                      "description": "The source types a create accepts."
                    },
                    "min_poll_interval": {
                      "type": "integer",
                      "description": "Seconds: the shortest interval this workspace's plan polls at (Pro 60, Team 30). A shorter one is saved as this."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid/revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Agent token carries no Hookie scope, or the email domain is not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found` \u2014 unknown project id, or it belongs to another workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID). Must belong to the caller's workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201)."
      },
      "post": {
        "operationId": "createSource",
        "summary": "Create a source connector",
        "tags": [
          "Sources"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "poll_interval"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "source": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "postgres",
                            "mysql",
                            "http"
                          ]
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "cursor_column": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "last_cursor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "poll_interval": {
                          "type": "integer",
                          "description": "Seconds."
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ],
                          "description": "Raw D1 integer, not a boolean."
                        },
                        "last_run_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "last_error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "created_at": {
                          "type": "string"
                        }
                      },
                      "description": "The created source, as GET by id returns it (#201)."
                    },
                    "poll_interval": {
                      "type": "integer",
                      "description": "Seconds, as stored: the request's interval clamped up to the plan minimum."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`name is required`; `type \"mysql\" is not supported: sources poll PostgreSQL only` (also `http`); `type must be \"postgres\"`; `config is required: {url: \"postgres://\u2026\", table}`; `config.url must be a postgres:// connection string`; `config.table is required` / `is not a valid identifier`; `cursor_column is required: \u2026` / `is not a valid column name`; or a dataset-name error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`Source connectors require the Pro plan or higher` (plan cap is 0), or `Plan limit reached (N sources)`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (write capability required), or `Missing X-Requested-With header` on a cookie session. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "dataset",
                  "config",
                  "cursor_column"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "postgres"
                    ],
                    "default": "postgres"
                  },
                  "dataset": {
                    "type": "string",
                    "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$",
                    "description": "Dataset the polled rows land in."
                  },
                  "config": {
                    "type": "object",
                    "required": [
                      "url",
                      "table"
                    ],
                    "additionalProperties": false,
                    "description": "Stored AES-256-GCM encrypted and never returned.",
                    "properties": {
                      "url": {
                        "type": "string",
                        "pattern": "^postgres(ql)?://",
                        "description": "Connection string. Use a read-only login."
                      },
                      "table": {
                        "type": "string",
                        "description": "Table or view, optionally schema-qualified (`public.orders`). Each part must match ^[A-Za-z_][A-Za-z0-9_]*$."
                      }
                    }
                  },
                  "cursor_column": {
                    "type": "string",
                    "pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
                    "description": "Required: a column that only increases (an id, or an updated-at time). Each poll reads rows past the last value it saw, in its order."
                  },
                  "poll_interval": {
                    "type": "integer",
                    "description": "Seconds. Clamped up to the plan minimum (Pro 60, Team 30); a non-numeric or missing value becomes the plan minimum."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "PostgreSQL only (#196, FLOW-3). Type, config and cursor column are validated as the poller uses them, before the plan's source cap is checked; MySQL and HTTP poll were offered but never implemented and are refused. The first poll runs on the next 5-minute heartbeat."
      }
    },
    "/admin/api/projects/{project_id}/sources/{source_id}": {
      "get": {
        "operationId": "getSource",
        "summary": "Get one source",
        "tags": [
          "Sources"
        ],
        "description": "The object, as one row of the list returns it, or 404. This used to answer 200 with the whole list for any id (DX-6, #201). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "source"
                  ],
                  "properties": {
                    "source": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "postgres",
                            "mysql",
                            "http"
                          ]
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "cursor_column": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "last_cursor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "poll_interval": {
                          "type": "integer",
                          "description": "Seconds."
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ],
                          "description": "Raw D1 integer, not a boolean."
                        },
                        "last_run_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "last_error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "created_at": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Source not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateSource",
        "summary": "Update a source connector",
        "tags": [
          "Sources"
        ],
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "poll_interval": {
                      "type": "integer",
                      "description": "Present when the interval changed: the value stored."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`body must be an object`; `Nothing to update: \u2026`; a type, config or cursor-column error as for create; `enabled must be a boolean`; `poll_interval must be a positive number of seconds`; or a dataset-name error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Source not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "402": {
            "description": "`Plan limit reached` - enabled: true while the workspace already runs as many sources as its plan allows (only possible after a downgrade paused some, #212). Upgrade, or turn another off first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1
                  },
                  "dataset": {
                    "type": "string",
                    "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$"
                  },
                  "poll_interval": {
                    "type": "integer",
                    "description": "Seconds, clamped up to the plan minimum."
                  },
                  "cursor_column": {
                    "type": "string",
                    "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "postgres"
                    ]
                  },
                  "config": {
                    "type": "object",
                    "properties": {
                      "url": {
                        "type": "string",
                        "pattern": "^postgres(ql)?://"
                      },
                      "table": {
                        "type": "string"
                      }
                    },
                    "description": "Merged over the stored config, then validated whole."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Every field a poll depends on can be corrected in place (#196, FLOW-3). A partial `config` (for example only `table`) is merged over the stored, encrypted one, so the connection string need not be sent again. A new table or cursor column restarts the watermark (`last_cursor` is cleared); any edit clears `last_error`."
      },
      "delete": {
        "operationId": "deleteSource",
        "summary": "Delete a source connector",
        "tags": [
          "Sources"
        ],
        "responses": {
          "200": {
            "description": "Deleted (hard delete).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Source not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/sources/{source_id}/test": {
      "post": {
        "operationId": "testSource",
        "summary": "Test a source connector's connection",
        "tags": [
          "Sources"
        ],
        "responses": {
          "200": {
            "description": "The source's table and cursor column are readable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Source not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The test failed. NOT the standard error body: it is the poller's own `{ok:false, error}` result (`poller unavailable` when the DO returned nothing parseable).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Connects with the stored config and reads the cursor column from the table (`SELECT \"<cursor>\" FROM <table> LIMIT 0`), so a pass means a poll can run, not only that the server answers (#196)."
      }
    },
    "/admin/api/projects/{project_id}/triggers": {
      "get": {
        "operationId": "listTriggers",
        "summary": "List agentic-AI event triggers in a project",
        "tags": [
          "Triggers"
        ],
        "responses": {
          "200": {
            "description": "Triggers, newest first. `match` and `config` are returned as the raw JSON strings stored in D1, not parsed objects.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "triggers"
                  ],
                  "properties": {
                    "triggers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "enabled": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ]
                          },
                          "match": {
                            "type": "string",
                            "description": "JSON string: {dataset, conditions?:[{path, op, value}]} (a trigger saved before #165 may hold [{path, equals}])."
                          },
                          "action_type": {
                            "type": "string",
                            "example": "ai_agent"
                          },
                          "config": {
                            "type": "string",
                            "description": "JSON string: {instructions|prompt, model?, output_dataset?, max_tokens?}"
                          },
                          "created_at": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201)."
      },
      "post": {
        "operationId": "createTrigger",
        "summary": "Create an agentic-AI event trigger",
        "tags": [
          "Triggers"
        ],
        "responses": {
          "201": {
            "description": "Created. Always enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "trigger": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "match": {
                          "type": "string",
                          "description": "JSON string: {dataset, conditions?:[{path, op, value}]} (a trigger saved before #165 may hold [{path, equals}])."
                        },
                        "action_type": {
                          "type": "string",
                          "example": "ai_agent"
                        },
                        "config": {
                          "type": "string",
                          "description": "JSON string: {instructions|prompt, model?, output_dataset?, max_tokens?}"
                        },
                        "created_at": {
                          "type": "string"
                        }
                      },
                      "description": "The created trigger, as GET by id returns it (#201)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`name, match, and config are required`, `match.dataset: <reason>`, `match.conditions: <reason>`, `action_type must be \"ai_agent\"`, `config.instructions is required: \u2026`, `config.model \"\u2026\" is not a supported model. \u2026`, `config.output_dataset: <reason>`, or `config.max_tokens must be a whole number from 1 to 1024`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "match",
                  "config"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "match": {
                    "type": "object",
                    "description": "Validated on save (#165): `dataset` must be a dataset name, and `conditions` use the one condition dialect; stored normalised to {path, op, value}. An empty or absent `conditions` array matches every event in the dataset. A trigger saved before #165 keeps its {path, equals} conditions, which compare as text.",
                    "properties": {
                      "dataset": {
                        "type": "string",
                        "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$"
                      },
                      "conditions": {
                        "type": "array",
                        "items": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/Condition"
                            }
                          ],
                          "description": "One condition dialect (#165): {path, op, value}, or {path, equals} as shorthand for op \"equals\". Refused at save: both forms at once, a missing value (except exists), gt/gte/lt/lte with a non-number, contains or regex with a non-string, a regex that does not compile or is unbounded, in without a list of 1-100 scalars, any other object or array value, an unknown key. A {{placeholder}} value is refused here: nothing resolves it, so it would be compared as literal text."
                        }
                      }
                    },
                    "required": [
                      "dataset"
                    ]
                  },
                  "config": {
                    "type": "object",
                    "required": [
                      "instructions"
                    ],
                    "description": "Validated on save (#196, FLOW-12) and stored normalised as {instructions, model?, output_dataset, max_tokens?}. It used to be stored as given and normalised silently at run time.",
                    "properties": {
                      "instructions": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 8000,
                        "description": "What the model does with each event (the system prompt). `prompt` is accepted as an alias."
                      },
                      "prompt": {
                        "type": "string",
                        "description": "Alias for instructions."
                      },
                      "model": {
                        "type": "string",
                        "enum": [
                          "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
                          "@cf/meta/llama-3.1-8b-instruct",
                          "@cf/meta/llama-3.1-8b-instruct-fast",
                          "@cf/meta/llama-3.2-3b-instruct",
                          "@cf/meta/llama-4-scout-17b-16e-instruct",
                          "@cf/mistralai/mistral-small-3.1-24b-instruct",
                          "@cf/google/gemma-3-12b-it"
                        ],
                        "description": "A model from GET /admin/api/ai-models. Absent or empty means the default (@cf/meta/llama-3.3-70b-instruct-fp8-fast); anything off the list is refused with 400 `\u2026 is not a supported model` (#196, FLOW-16). It used to be replaced by the default without a word."
                      },
                      "output_dataset": {
                        "type": "string",
                        "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$",
                        "default": "agent_output",
                        "description": "Where the answer is stored as a record. An invalid name is refused. The record goes through the shared fan-out (#206): it is delivered to the project's destinations and streamed, but fires no AI trigger and starts no workflow, so a trigger matching its own output cannot loop."
                      },
                      "max_tokens": {
                        "type": "integer",
                        "minimum": 1,
                        "description": "Longest answer; above 1024 is clamped to 1024."
                      }
                    }
                  },
                  "action_type": {
                    "type": "string",
                    "enum": [
                      "ai_agent"
                    ],
                    "default": "ai_agent",
                    "description": "The one action there is; anything else is refused."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/triggers/{trigger_id}": {
      "get": {
        "operationId": "getTrigger",
        "summary": "Get one trigger",
        "tags": [
          "Triggers"
        ],
        "description": "The object, as one row of the list returns it, or 404. This used to answer 200 with the whole list for any id (DX-6, #201). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "trigger"
                  ],
                  "properties": {
                    "trigger": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "match": {
                          "type": "string",
                          "description": "JSON string: {dataset, conditions?:[{path, op, value}]} (a trigger saved before #165 may hold [{path, equals}])."
                        },
                        "action_type": {
                          "type": "string",
                          "example": "ai_agent"
                        },
                        "config": {
                          "type": "string",
                          "description": "JSON string: {instructions|prompt, model?, output_dataset?, max_tokens?}"
                        },
                        "created_at": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateTrigger",
        "summary": "Enable or disable an AI trigger",
        "tags": [
          "Triggers"
        ],
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`Only 'enabled' can be toggled`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "delete": {
        "operationId": "deleteTrigger",
        "summary": "Delete an AI trigger",
        "tags": [
          "Triggers"
        ],
        "responses": {
          "200": {
            "description": "Deleted (hard delete).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/cron-triggers": {
      "get": {
        "operationId": "listCronTriggers",
        "summary": "List cron triggers in a project",
        "tags": [
          "Cron triggers"
        ],
        "responses": {
          "200": {
            "description": "Cron triggers, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "cron_triggers"
                  ],
                  "properties": {
                    "cron_triggers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "enabled": {
                            "type": "boolean"
                          },
                          "schedule": {
                            "type": "string",
                            "description": "5-field cron expression, read in `timezone`."
                          },
                          "timezone": {
                            "type": "string",
                            "description": "IANA time zone the schedule is read in (#165); UTC for triggers created before it."
                          },
                          "payload": {
                            "type": "string",
                            "description": "JSON template string with {{now}}/{{iso}}/{{ts}} substitutions."
                          },
                          "dataset": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "last_run_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "next_run_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "When the sweep next fires it (UTC instant). Recomputed on a schedule or zone change and on resume."
                          },
                          "last_error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201)."
      },
      "post": {
        "operationId": "createCronTrigger",
        "summary": "Create a cron trigger",
        "tags": [
          "Cron triggers"
        ],
        "responses": {
          "200": {
            "description": "Created and enabled. NOTE: this handler returns 200, not 201.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "timezone",
                    "next_run_at",
                    "next_runs"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "next_run_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "timezone": {
                      "type": "string"
                    },
                    "next_runs": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "description": "The next three fire times (UTC instants)."
                    },
                    "cron_trigger": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "schedule": {
                          "type": "string",
                          "description": "5-field cron expression, read in `timezone`."
                        },
                        "timezone": {
                          "type": "string",
                          "description": "IANA time zone the schedule is read in (#165); UTC for triggers created before it."
                        },
                        "payload": {
                          "type": "string",
                          "description": "JSON template string with {{now}}/{{iso}}/{{ts}} substitutions."
                        },
                        "dataset": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "last_run_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "next_run_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "When the sweep next fires it (UTC instant). Recomputed on a schedule or zone change and on resume."
                        },
                        "last_error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "created_at": {
                          "type": "string"
                        }
                      },
                      "description": "The created cron trigger, as GET by id returns it (#201)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`name is required (1-80 chars)`, `bad schedule: <reason>` (including a schedule that fires less than 5 minutes apart), `timezone must be an IANA time zone \u2026`, a dataset that cannot store a record, `payload must be valid JSON (after {{}} substitution)`, or `payload has no fields`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 owner or admin only (not developer), or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The project already has 10 cron triggers, the most a project may hold. Delete one before adding another.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "schedule"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80,
                    "description": "Trimmed before validation."
                  },
                  "schedule": {
                    "type": "string",
                    "description": "5-field cron expression (minute hour day-of-month month day-of-week), read in `timezone`. Two firings may not be closer than 5 minutes (the scheduler's tick): `* * * * *`, `*/2 * * * *` and `0,58 * * * *` are refused."
                  },
                  "payload": {
                    "description": "JSON template fired on each run. A string is stored verbatim; any other value is JSON.stringify'd; omitted becomes '{}'. Must still parse as JSON after {{now}}/{{iso}}/{{ts}} substitution.",
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "object"
                      },
                      {
                        "type": "array"
                      }
                    ]
                  },
                  "dataset": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Optional identity dataset: every run is stored in it as one record. When absent, each run is routed through the project's mapping rules, and a run none of them matches is stored whole, as one record, in the dataset `default` (#284) rather than kept as a submission with no record."
                  },
                  "timezone": {
                    "type": "string",
                    "default": "UTC",
                    "description": "IANA time zone, e.g. America/New_York (#165, FLOW-14). The schedule keeps local time across daylight-saving changes: a local time that happens twice fires once (its first occurrence); one skipped by the spring change fires just after the gap. Normalised to its canonical name."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/cron-triggers/preview": {
      "post": {
        "operationId": "previewCronSchedule",
        "summary": "Preview a cron schedule's next fire times",
        "tags": [
          "Cron triggers"
        ],
        "responses": {
          "200": {
            "description": "The schedule's next five fire times.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "timezone",
                    "next_runs"
                  ],
                  "properties": {
                    "timezone": {
                      "type": "string",
                      "description": "Canonical zone name."
                    },
                    "next_runs": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`bad schedule: <reason>` or `timezone must be an IANA time zone \u2026`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Missing X-Requested-With header`, or the caller cannot reach the project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "schedule"
                ],
                "properties": {
                  "schedule": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string",
                    "default": "UTC"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "The next five fire times of a schedule in a time zone, before it is saved. Checks the schedule exactly as create does. Any project member."
      }
    },
    "/admin/api/projects/{project_id}/cron-triggers/{cron_trigger_id}": {
      "get": {
        "operationId": "getCronTrigger",
        "summary": "Get one cron trigger",
        "tags": [
          "Cron triggers"
        ],
        "description": "The object, as one row of the list returns it, or 404. This used to answer 200 with the whole list for any id (DX-6, #201). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cron_trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "cron_trigger"
                  ],
                  "properties": {
                    "cron_trigger": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "schedule": {
                          "type": "string",
                          "description": "5-field cron expression, read in `timezone`."
                        },
                        "timezone": {
                          "type": "string",
                          "description": "IANA time zone the schedule is read in (#165); UTC for triggers created before it."
                        },
                        "payload": {
                          "type": "string",
                          "description": "JSON template string with {{now}}/{{iso}}/{{ts}} substitutions."
                        },
                        "dataset": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "last_run_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "next_run_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "When the sweep next fires it (UTC instant). Recomputed on a schedule or zone change and on resume."
                        },
                        "last_error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "created_at": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Cron trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateCronTrigger",
        "summary": "Update a cron trigger",
        "tags": [
          "Cron triggers"
        ],
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "cron_trigger",
                    "next_runs"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "cron_trigger": {
                      "type": "object",
                      "description": "The trigger as the list returns it, after the change."
                    },
                    "next_runs": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "description": "The next three fire times; empty while paused."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`Nothing to change: send any of \u2026`, `enabled must be true or false`, or any error create returns for the field changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 owner or admin only, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Cron trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cron_trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "schedule": {
                    "type": "string",
                    "description": "As on create, including the 5-minute floor."
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA time zone."
                  },
                  "payload": {
                    "description": "As on create.",
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "object"
                      },
                      {
                        "type": "array"
                      }
                    ]
                  },
                  "dataset": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "As on create. null or empty routes each run through the project's mapping rules, with `default` for a run none of them matches (#284)."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Change any of name, enabled, schedule, timezone, payload and dataset (#165); each is checked as create checks it. A new schedule or time zone recomputes next_run_at, and so does resuming (enabled false to true), so a paused trigger does not fire at once on the next sweep for a time that passed while it was paused."
      },
      "delete": {
        "operationId": "deleteCronTrigger",
        "summary": "Delete a cron trigger",
        "tags": [
          "Cron triggers"
        ],
        "responses": {
          "200": {
            "description": "Deleted (hard delete).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 owner or admin only, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Cron trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cron_trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/cron-triggers/{cron_trigger_id}/test": {
      "post": {
        "operationId": "testCronTrigger",
        "summary": "Fire a cron trigger once, immediately",
        "tags": [
          "Cron triggers"
        ],
        "responses": {
          "200": {
            "description": "Fired. A real submission + record is written (for a trigger with no dataset, the records its rules write, or one record in `default` when no rule matches, #284) and the full fan-out (deliveries, live stream, AI triggers, workflows) runs. `next_run_at` is NOT advanced.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "fired"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "fired": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 owner or admin only, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Cron trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`test fire failed: <reason>`, or `Internal error`. For a trigger with no dataset whose run no rule stores, the reason is `no rule stored the event, and storing it in \"default\" failed: <why>` (e.g. `record has no fields` for an empty payload); the submission is kept, marked failed (#284).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cron_trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/ws-triggers": {
      "get": {
        "operationId": "listWsTriggers",
        "summary": "List WebSocket triggers in a project",
        "tags": [
          "WebSocket triggers"
        ],
        "responses": {
          "200": {
            "description": "WebSocket triggers, newest first. The encrypted `config` (url, headers, subprotocols, connect_message) is deliberately never returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ws_triggers"
                  ],
                  "properties": {
                    "ws_triggers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "enabled": {
                            "type": "boolean"
                          },
                          "conditions": {
                            "type": "array",
                            "items": {
                              "allOf": [
                                {
                                  "$ref": "#/components/schemas/Condition"
                                }
                              ],
                              "description": "Stored in the full {path, op, value} form."
                            }
                          },
                          "mappings": {
                            "type": [
                              "array",
                              "null"
                            ],
                            "items": {
                              "type": "object"
                            }
                          },
                          "dataset": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "connected",
                              "connecting",
                              "disconnected",
                              "error"
                            ]
                          },
                          "last_event_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "last_error": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Why the trigger is not storing events, or null. A connection failure (cleared on the next connect), a count of matching frames dropped by the rate limit or the quota, or the reason a matching frame could not be stored (#294); that last is cleared by the next frame that is stored."
                          },
                          "created_at": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201)."
      },
      "post": {
        "operationId": "createWsTrigger",
        "summary": "Create a WebSocket trigger",
        "tags": [
          "WebSocket triggers"
        ],
        "responses": {
          "200": {
            "description": "Created, enabled, and the WsListener Durable Object is armed immediately. NOTE: this handler returns 200, not 201.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "ws_trigger": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "conditions": {
                          "type": "array",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/Condition"
                              }
                            ],
                            "description": "Stored in the full {path, op, value} form."
                          }
                        },
                        "mappings": {
                          "type": [
                            "array",
                            "null"
                          ],
                          "items": {
                            "type": "object"
                          }
                        },
                        "dataset": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "connected",
                            "connecting",
                            "disconnected",
                            "error"
                          ]
                        },
                        "last_event_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "last_error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "created_at": {
                          "type": "string"
                        }
                      },
                      "description": "The created ws trigger, as GET by id returns it (#201)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`name is required (1-80 chars)`, `url must be a ws:// or wss:// URL`, a dataset that cannot store a record, or `conditions: <reason>` for a condition outside the one dialect, or `mappings: <reason>` for a mapping a rule would refuse (a bad path, a key that is not an identifier, a key mapped twice, too many).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 owner or admin only, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The project already has 5 WebSocket triggers, the most a project may hold. Delete one before adding another.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "url"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "url": {
                    "type": "string",
                    "description": "Must start with ws:// or wss://. Encrypted at rest with the rest of the connection config."
                  },
                  "headers": {
                    "type": "object",
                    "default": {},
                    "description": "Connection headers. Encrypted; never returned."
                  },
                  "subprotocols": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "default": []
                  },
                  "connect_message": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Sent once on connect (e.g. a subscribe frame)."
                  },
                  "conditions": {
                    "type": "array",
                    "default": [],
                    "description": "Frame filter; an empty array matches every frame. Validated on save (#165) and stored in the full {path, op, value} form.",
                    "items": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Condition"
                        }
                      ],
                      "description": "One condition dialect (#165): {path, op, value}, or {path, equals} as shorthand for op \"equals\". Refused at save: both forms at once, a missing value (except exists), gt/gte/lt/lte with a non-number, contains or regex with a non-string, a regex that does not compile or is unbounded, in without a list of 1-100 scalars, any other object or array value, an unknown key. A {{placeholder}} value is refused here: nothing resolves it, so it would be compared as literal text."
                    }
                  },
                  "mappings": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "object",
                      "required": [
                        "path",
                        "key"
                      ],
                      "properties": {
                        "path": {
                          "type": "string"
                        },
                        "key": {
                          "type": "string"
                        }
                      },
                      "additionalProperties": false
                    },
                    "description": "Optional [{path,key}] field mappings, validated as a rule's are and applied to every matching frame the way a rule's are (#228): each picks a path out of the frame into a key of the event, which is what is filed into `dataset` or handed to the project's rules, and what `default` keeps when no rule matches (#284). The submission keeps the frame as it arrived. An empty array or null keeps the frame unchanged (identity)."
                  },
                  "dataset": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Optional dataset every matching frame is stored in, as one record (after `mappings`). null or absent routes each frame through the project's mapping rules, and a frame none of them matches is stored whole, as one record, in the dataset `default` (#284) rather than kept as a submission with no record. Validated as a dataset name."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/ws-triggers/{ws_trigger_id}": {
      "get": {
        "operationId": "getWsTrigger",
        "summary": "Get one ws trigger",
        "tags": [
          "WebSocket triggers"
        ],
        "description": "The object, as one row of the list returns it, or 404. This used to answer 200 with the whole list for any id (DX-6, #201). Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ws_trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The object.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ws_trigger"
                  ],
                  "properties": {
                    "ws_trigger": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "conditions": {
                          "type": "array",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/Condition"
                              }
                            ],
                            "description": "Stored in the full {path, op, value} form."
                          }
                        },
                        "mappings": {
                          "type": [
                            "array",
                            "null"
                          ],
                          "items": {
                            "type": "object"
                          }
                        },
                        "dataset": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "connected",
                            "connecting",
                            "disconnected",
                            "error"
                          ]
                        },
                        "last_event_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "last_error": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Why the trigger is not storing events, or null. A connection failure (cleared on the next connect), a count of matching frames dropped by the rate limit or the quota, or the reason a matching frame could not be stored (#294); that last is cleared by the next frame that is stored."
                        },
                        "created_at": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`WS trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateWsTrigger",
        "summary": "Edit, pause or resume a WebSocket trigger",
        "tags": [
          "WebSocket triggers"
        ],
        "responses": {
          "200": {
            "description": "Updated. The listener is reconnected, stopped or armed to match. The trigger as stored now, never its connection settings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "ws_trigger"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "ws_trigger": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "conditions": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "path": {
                                "type": "string"
                              },
                              "op": {
                                "type": "string",
                                "enum": [
                                  "equals",
                                  "not_equals",
                                  "contains",
                                  "exists",
                                  "gt",
                                  "lt"
                                ]
                              },
                              "value": {}
                            },
                            "description": "Stored in the full {path, op, value} form."
                          }
                        },
                        "mappings": {
                          "type": [
                            "array",
                            "null"
                          ],
                          "items": {
                            "type": "object"
                          }
                        },
                        "dataset": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "connected",
                            "connecting",
                            "disconnected",
                            "error"
                          ]
                        },
                        "last_event_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "last_error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "created_at": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`Nothing to change: send any of name, enabled, url, headers, subprotocols, connect_message, conditions, dataset`, `enabled must be true or false`, `name is required (1-80 chars)`, `conditions: \u2026` (the #165 validation), `dataset: \u2026`, `url must be a ws:// or wss:// URL`, or `url moves this trigger to another host: send headers and connect_message with it (null for none). \u2026`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 owner or admin only, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`WS trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`This trigger's stored connection settings could not be read`, or `Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ws_trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "additionalProperties": true,
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "true arms the listener socket; false tears it down."
                  },
                  "url": {
                    "type": "string",
                    "description": "Must start with ws:// or wss://. To a different host, send headers and connect_message too when the trigger has them stored."
                  },
                  "headers": {
                    "type": "object",
                    "default": {},
                    "description": "Connection headers. Encrypted; never returned."
                  },
                  "subprotocols": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "default": []
                  },
                  "connect_message": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Sent once on connect (e.g. a subscribe frame)."
                  },
                  "conditions": {
                    "type": "array",
                    "default": [],
                    "description": "Frame filter; an empty array matches every frame. Validated on save (#165) and stored in the full {path, op, value} form.",
                    "items": {
                      "type": "object",
                      "required": [
                        "path"
                      ],
                      "additionalProperties": false,
                      "description": "One condition dialect (#165): {path, op, value}, or {path, equals} as shorthand for op \"equals\". Refused at save: both forms at once, a missing value (except exists), gt/lt with a non-number, contains with a non-string, an object or array value, an unknown key. A {{placeholder}} value is refused here: nothing resolves it, so it would be compared as literal text.",
                      "properties": {
                        "path": {
                          "type": "string",
                          "minLength": 1,
                          "description": "Dot path. The empty path is rejected."
                        },
                        "op": {
                          "type": "string",
                          "enum": [
                            "equals",
                            "not_equals",
                            "contains",
                            "exists",
                            "gt",
                            "lt"
                          ]
                        },
                        "value": {
                          "description": "equals/not_equals compare with strict ===/!== (5 and \"5\" differ); contains needs a string; gt/lt need a number; exists takes none."
                        },
                        "equals": {
                          "description": "Shorthand for op \"equals\" with this value. Not together with op or value."
                        }
                      }
                    }
                  },
                  "dataset": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "null or empty routes each frame by the project's rules, with `default` for a frame none of them matches (#284). Validated as a dataset name."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Change any of name, enabled, url, headers, subprotocols, connect_message, conditions and dataset (#177); each is checked as create checks it, conditions in the one dialect (#165). A change to the connection, the conditions or the dataset reconnects the listener, which reads them when it connects; a rename alone leaves the socket open. enabled false closes the socket and true opens it. The connection settings stay encrypted and are never returned. Moving url to a different host is refused unless the request also sends each stored headers / connect_message (null for none): credentials nobody can read back are never carried to a new host unseen."
      },
      "delete": {
        "operationId": "deleteWsTrigger",
        "summary": "Delete a WebSocket trigger",
        "tags": [
          "WebSocket triggers"
        ],
        "responses": {
          "200": {
            "description": "Deleted (hard delete) and the listener socket torn down.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 owner or admin only, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`WS trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ws_trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/ws-triggers/{ws_trigger_id}/test": {
      "post": {
        "operationId": "testWsTrigger",
        "summary": "Probe a WebSocket trigger's connection",
        "tags": [
          "WebSocket triggers"
        ],
        "responses": {
          "200": {
            "description": "Short-lived probe succeeded. The body is the WsListener DO's own result \u2014 `ok` is the only guaranteed field; the DO may include the first frames it saw.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "additionalProperties": true
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 owner or admin only, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`WS trigger not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Probe failed. NOT the standard error body: it is the listener's `{ok:false, \u2026}` result (`{ok:false, error:\"listener unavailable\"}` when the DO returned nothing parseable).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": true
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ws_trigger_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/workflows": {
      "get": {
        "operationId": "listWorkflows",
        "summary": "List workflow definitions in a project",
        "tags": [
          "Workflows"
        ],
        "responses": {
          "200": {
            "description": "Workflow definitions, newest first. Soft-deleted workflows (active = 0) are still listed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workflows"
                  ],
                  "properties": {
                    "workflows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "version": {
                            "type": "integer",
                            "description": "Starts at 1; incremented on every update."
                          },
                          "active": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ]
                          },
                          "entry_dataset": {
                            "type": "string"
                          },
                          "entry_conditions": {
                            "type": "string",
                            "description": "Raw JSON string of the condition array."
                          },
                          "steps": {
                            "type": "string",
                            "description": "Raw JSON string of the normalized step array."
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "updated_at": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201)."
      },
      "post": {
        "operationId": "createWorkflow",
        "summary": "Create a workflow definition",
        "tags": [
          "Workflows"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "ok"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "workflow": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "version": {
                          "type": "integer",
                          "description": "Starts at 1; incremented on every update."
                        },
                        "active": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "entry_dataset": {
                          "type": "string"
                        },
                        "entry_conditions": {
                          "type": "string",
                          "description": "Raw JSON string of the condition array."
                        },
                        "steps": {
                          "type": "string",
                          "description": "Raw JSON string of the normalized step array."
                        },
                        "created_at": {
                          "type": "string"
                        },
                        "updated_at": {
                          "type": "string"
                        }
                      },
                      "description": "The created workflow, as GET by id returns it (#201)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failure, one message. Examples: `body must be an object`; `name is required (<=120 chars)`; `entry_dataset: <dataset name error>`; `conditions must be an array`; `condition.path is required`; `condition.op must be one of equals|not_equals|contains|exists|gt|lt`; `steps must be an array`; `at least one step is required`; `too many steps (max 32 including branch sub-steps)`; `step N: unknown type \"x\"`; `step N (call_ai): instructions/prompt is required`; `step N (delay): seconds must be > 0`; `step N (wait_for_event): not allowed inside a branch (branches run atomically)`; `step N (branch): max nesting depth is 3`; `step N (branch): then must contain at least one step`; `step N (delay): seconds must be at most 31536000 (365 days)`; `step N (http): url must be https`; `step N (http): a {{placeholder}} may only appear after the host, \u2026`; `step N (http): url host \"\u2026\" is a private or reserved address`; `step N (http): a GET request cannot carry a body`; `step N (set): values must set at least one key`; `step N (transform): mapping.x must be a context path such as \"entry.email\"`; `steps are too large (max 262144 bytes serialized)`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (write capability required), or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "entry_dataset",
                  "steps"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120,
                    "description": "Trimmed before validation."
                  },
                  "slug": {
                    "type": "string",
                    "description": "Slugified server-side: lowercased, non-alphanumerics collapsed to '-', trimmed, max 48 chars, falling back to 'workflow'. Defaults to a slug of `name`. Not uniqueness-checked."
                  },
                  "active": {
                    "type": "boolean",
                    "default": true,
                    "description": "Only the literal false deactivates; anything else (including omission) means active. An inactive workflow starts no instances."
                  },
                  "entry_dataset": {
                    "type": "string",
                    "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$",
                    "description": "A record landing in this dataset via the ingest/cron/ws pipeline can start an instance. Records emitted BY a workflow never start new ones."
                  },
                  "entry_conditions": {
                    "type": "array",
                    "default": [],
                    "description": "Evaluated against the entry record. An empty array matches every record in the dataset.",
                    "items": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Condition"
                        }
                      ],
                      "description": "One condition dialect (#165): {path, op, value}, or {path, equals} as shorthand for op \"equals\". Refused at save: both forms at once, a missing value (except exists), gt/gte/lt/lte with a non-number, contains or regex with a non-string, a regex that does not compile or is unbounded, in without a list of 1-100 scalars, any other object or array value, an unknown key. A {{placeholder}} value is refused here: nothing resolves it, so it would be compared as literal text."
                    }
                  },
                  "steps": {
                    "type": "array",
                    "minItems": 1,
                    "description": "Ordered steps. Total count INCLUDING nested branch sub-steps must be <= 32, and the steps serialized at most 262144 bytes. New instances run on Cloudflare Workflows: each step is a durable step with retries, so emit_event is keyed on (instance, step) and never emits twice.",
                    "items": {
                      "oneOf": [
                        {
                          "title": "emit_event",
                          "type": "object",
                          "required": [
                            "type",
                            "dataset"
                          ],
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "emit_event"
                              ]
                            },
                            "dataset": {
                              "type": "string",
                              "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$"
                            },
                            "payload": {
                              "type": "object",
                              "description": "Must be a plain object (not an array/null). Merged OVER the entry record's fields - payload wins. String leaves are templated from the instance context (#165, FLOW-6): \"{{triage.summary}}\" alone takes the value with its type, {{path}} inside longer text is replaced as text. A placeholder that resolves to nothing leaves its field out; one whose root no earlier step writes (entry, an output_key, a set key, waited_event) is refused at save."
                            },
                            "deliver": {
                              "type": "boolean",
                              "default": true,
                              "description": "Send the emitted record through the shared fan-out: the project's destinations (under the delivery allowance), the live stream and AI triggers, never a workflow. It is written with a fan-out outbox row, so a fan-out that fails is re-driven by the sweep (#206). Only the literal false disables it; the record is then only stored and streamed."
                            }
                          }
                        },
                        {
                          "title": "call_ai",
                          "type": "object",
                          "required": [
                            "type"
                          ],
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "call_ai"
                              ]
                            },
                            "instructions": {
                              "type": "string",
                              "description": "Required \u2014 but `prompt` is accepted as an alias. If both are absent or empty the step is rejected."
                            },
                            "prompt": {
                              "type": "string",
                              "description": "Alias for `instructions`; normalized into `instructions` on write."
                            },
                            "model": {
                              "type": "string",
                              "description": "A model from GET /admin/api/ai-models. Absent or empty means the default (@cf/meta/llama-3.3-70b-instruct-fp8-fast); anything off the list is refused with 400 `\u2026 is not a supported model` (#196, FLOW-16). It used to be replaced by the default without a word."
                            },
                            "max_tokens": {
                              "type": "integer",
                              "description": "Clamped to 1..1024. A non-integer is dropped."
                            },
                            "output_key": {
                              "type": "string",
                              "default": "ai_result",
                              "description": "Instance-context key the reply is stored under. A reply that is JSON is stored parsed - bare, in a ```json fence, or after a preface such as 'Here is the JSON:' (#165, FLOW-9) - so a branch can read its fields; anything else stays text."
                            },
                            "output_dataset": {
                              "type": "string",
                              "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$",
                              "description": "Optional dataset to also write the AI result into, as a record {output, workflow_id, instance_id} carrying the entry event's hop. The record goes through the shared fan-out like any other event, with a fan-out outbox row behind it (#177, FLOW-6): the project's destinations whose filter admits the dataset (under the monthly delivery allowance), the live stream and matching AI triggers. It never starts or resumes a workflow, not even one entered on this dataset, so a workflow cannot feed itself."
                            }
                          }
                        },
                        {
                          "title": "wait_for_event",
                          "type": "object",
                          "required": [
                            "type",
                            "dataset"
                          ],
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "wait_for_event"
                              ]
                            },
                            "dataset": {
                              "type": "string",
                              "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$"
                            },
                            "conditions": {
                              "type": "array",
                              "default": [],
                              "description": "Read against the ARRIVING record. A value may reference this run's context (see items).",
                              "items": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/Condition"
                                  }
                                ],
                                "description": "One condition dialect (#165): {path, op, value}, or {path, equals} as shorthand for op \"equals\". Refused at save: both forms at once, a missing value (except exists), gt/gte/lt/lte with a non-number, contains or regex with a non-string, a regex that does not compile or is unbounded, in without a list of 1-100 scalars, any other object or array value, an unknown key. A value may be a {{placeholder}} naming the instance context (e.g. \"{{entry.order_id}}\"), resolved when the wait is armed, so the wait matches this instance's record only; a placeholder that resolves to nothing fails the run."
                              }
                            },
                            "timeout_seconds": {
                              "type": "integer",
                              "minimum": 1,
                              "description": "Must be > 0; floored; at most 31536000 (365 days). Omitted: on the Cloudflare Workflows engine the wait times out after 365 days (step.waitForEvent needs a finite timeout); on the legacy engine it never does. What a timeout does is on_timeout.",
                              "maximum": 31536000
                            },
                            "on_timeout": {
                              "type": "string",
                              "enum": [
                                "fail",
                                "continue"
                              ],
                              "default": "fail",
                              "description": "fail (default): a timed-out wait ends the instance timed_out. continue (#165): the run carries on with waited_event set to null, so a following branch on {\"path\":\"waited_event\",\"op\":\"exists\"} can take the no-reply path. Both engines."
                            }
                          },
                          "description": "NOT allowed inside a branch."
                        },
                        {
                          "title": "delay",
                          "type": "object",
                          "required": [
                            "type",
                            "seconds"
                          ],
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "delay"
                              ]
                            },
                            "seconds": {
                              "type": "integer",
                              "minimum": 1,
                              "description": "Must be > 0; floored; at most 31536000 (365 days, Cloudflare's step.sleep maximum). On the Cloudflare Workflows engine the sleep is accurate to the second; a legacy instance still resumes on the ~5-minute sweep.",
                              "maximum": 31536000
                            }
                          },
                          "description": "NOT allowed inside a branch."
                        },
                        {
                          "title": "agent_call",
                          "type": "object",
                          "required": [
                            "type",
                            "agent_id"
                          ],
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "agent_call"
                              ]
                            },
                            "agent_id": {
                              "type": "string",
                              "minLength": 1,
                              "description": "Id of a project-scoped ai_agent. Existence is NOT checked at write time \u2014 a missing or inactive agent fails the step at run time."
                            },
                            "output_key": {
                              "type": "string",
                              "default": "agent_result"
                            }
                          }
                        },
                        {
                          "title": "branch",
                          "type": "object",
                          "required": [
                            "type",
                            "conditions",
                            "then"
                          ],
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "branch"
                              ]
                            },
                            "conditions": {
                              "type": "array",
                              "minItems": 1,
                              "description": "At least one condition is required. Evaluated against the instance CONTEXT (entry payload under `entry`, plus each earlier step's output: call_ai/agent_call/http/transform output_key, every key a set step writes, `waited_event`).",
                              "items": {
                                "allOf": [
                                  {
                                    "$ref": "#/components/schemas/Condition"
                                  }
                                ],
                                "description": "One condition dialect (#165): {path, op, value}, or {path, equals} as shorthand for op \"equals\". Refused at save: both forms at once, a missing value (except exists), gt/gte/lt/lte with a non-number, contains or regex with a non-string, a regex that does not compile or is unbounded, in without a list of 1-100 scalars, any other object or array value, an unknown key. A value may be a {{placeholder}} naming the instance context (e.g. \"{{entry.order_id}}\"), resolved each time the branch is evaluated."
                              }
                            },
                            "then": {
                              "type": "array",
                              "minItems": 1,
                              "description": "Sub-steps run when the conditions match. Only types that complete in one step are allowed: emit_event, call_ai, agent_call, http, set, transform, branch."
                            },
                            "else": {
                              "type": "array",
                              "description": "Optional. Same restrictions as `then`. An empty array is normalized away to absent."
                            }
                          },
                          "description": "Max nesting depth 3."
                        },
                        {
                          "title": "http",
                          "type": "object",
                          "required": [
                            "type",
                            "method",
                            "url"
                          ],
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "http"
                              ]
                            },
                            "method": {
                              "type": "string",
                              "enum": [
                                "GET",
                                "POST",
                                "PUT",
                                "PATCH",
                                "DELETE"
                              ],
                              "description": "Case-insensitive on write; stored upper-case."
                            },
                            "url": {
                              "type": "string",
                              "maxLength": 2048,
                              "description": "https to a PUBLIC host: no localhost, internal-only or single-label names, no private, loopback, link-local, CGNAT or multicast IP literals, no credentials in the URL. {{placeholders}} may appear only after the host; embedded values are URL-encoded. The resolved URL is checked again before every request. Redirects are never followed: a 3xx is returned as the result. Templating: a string that is exactly \"{{path}}\" takes the instance-context value at that dotted path with its type kept; {{path}} inside a longer string is replaced by the value as text (objects as JSON, null/missing as empty)."
                            },
                            "headers": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "string",
                                "maxLength": 4096
                              },
                              "maxProperties": 32,
                              "description": "Header values are PLAIN CONFIGURATION, stored with the definition and returned by GET: do not put secrets in them. Values are templated. Host, Content-Length, Connection, Transfer-Encoding, CF-Connecting-IP and X-Forwarded-For are refused. User-Agent and Idempotency-Key (<instance id>:<step path>, stable across retries) are sent unless set here."
                            },
                            "body": {
                              "description": "JSON, at most 65536 bytes serialized; not allowed on GET. String leaves are templated. A string body is sent as text/plain, anything else as application/json, unless Content-Type is set."
                            },
                            "output_key": {
                              "type": "string",
                              "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,63}$",
                              "default": "http_result",
                              "description": "Context key the result {status, body} is written to. body is the response parsed as JSON when it is JSON, else text, capped at 65536 bytes. A non-2xx status is a result, not a failure; only a network error or the timeout fails the step (retried twice)."
                            },
                            "timeout_seconds": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 30,
                              "default": 10
                            }
                          }
                        },
                        {
                          "title": "set",
                          "type": "object",
                          "required": [
                            "type",
                            "values"
                          ],
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "set"
                              ]
                            },
                            "values": {
                              "type": "object",
                              "minProperties": 1,
                              "maxProperties": 64,
                              "description": "Each key (letters, digits, underscores; not `entry`) is written into the instance context. Templating: a string that is exactly \"{{path}}\" takes the instance-context value at that dotted path with its type kept; {{path}} inside a longer string is replaced by the value as text (objects as JSON, null/missing as empty). Anything else is a literal. At most 262144 bytes serialized."
                            }
                          }
                        },
                        {
                          "title": "transform",
                          "type": "object",
                          "required": [
                            "type",
                            "mapping"
                          ],
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": [
                                "transform"
                              ]
                            },
                            "mapping": {
                              "type": "object",
                              "minProperties": 1,
                              "maxProperties": 64,
                              "additionalProperties": {
                                "type": "string",
                                "minLength": 1
                              },
                              "description": "Output key \u2192 dotted instance-context path (e.g. \"entry.customer.email\"). Builds one object; a missing path gives undefined."
                            },
                            "output_key": {
                              "type": "string",
                              "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,63}$",
                              "default": "transformed"
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/workflows/{workflow_id}": {
      "get": {
        "operationId": "getWorkflow",
        "summary": "Get one workflow definition",
        "tags": [
          "Workflows"
        ],
        "responses": {
          "200": {
            "description": "The definition. `entry_conditions` and `steps` are raw JSON strings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workflow"
                  ],
                  "properties": {
                    "workflow": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "version": {
                          "type": "integer"
                        },
                        "active": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "entry_dataset": {
                          "type": "string"
                        },
                        "entry_conditions": {
                          "type": "string"
                        },
                        "steps": {
                          "type": "string"
                        },
                        "created_at": {
                          "type": "string"
                        },
                        "updated_at": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Workflow not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "put": {
        "operationId": "updateWorkflow",
        "summary": "Replace a workflow definition",
        "tags": [
          "Workflows"
        ],
        "responses": {
          "200": {
            "description": "Replaced; `version` incremented.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Same validation messages as createWorkflow. Also a condition outside the one dialect, or a {{placeholder}} that can never resolve.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Workflow not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "entry_dataset",
                  "steps"
                ],
                "description": "Identical to the create body and validated by the same validateWorkflowSpec \u2014 this is a FULL REPLACE, not a merge: every omitted field reverts to its default (e.g. omitting `active` re-activates, omitting `entry_conditions` clears them). PATCH is accepted on this path and behaves identically. `version` is incremented server-side; it cannot be set.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "slug": {
                    "type": "string"
                  },
                  "active": {
                    "type": "boolean",
                    "default": true
                  },
                  "entry_dataset": {
                    "type": "string",
                    "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$"
                  },
                  "entry_conditions": {
                    "type": "array",
                    "default": [],
                    "items": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Condition"
                        }
                      ],
                      "description": "One condition dialect (#165): {path, op, value}, or {path, equals} as shorthand for op \"equals\". Refused at save: both forms at once, a missing value (except exists), gt/gte/lt/lte with a non-number, contains or regex with a non-string, a regex that does not compile or is unbounded, in without a list of 1-100 scalars, any other object or array value, an unknown key. A {{placeholder}} value is refused here: nothing resolves it, so it would be compared as literal text."
                    }
                  },
                  "steps": {
                    "type": "array",
                    "minItems": 1,
                    "description": "Same nine step types and same limits as createWorkflow (<= 32 steps including branch sub-steps; branch depth <= 3; no delay/wait_for_event inside a branch; delay and wait timeout <= 365 days; steps <= 262144 bytes serialized).",
                    "items": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "The version goes up only when the steps, entry dataset or entry conditions change."
      },
      "patch": {
        "operationId": "patchWorkflow",
        "summary": "Change part of a workflow (merge)",
        "tags": [
          "Workflows"
        ],
        "responses": {
          "200": {
            "description": "Replaced; `version` incremented.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Same validation messages as createWorkflow. Also a condition outside the one dialect, or a {{placeholder}} that can never resolve.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Workflow not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Identical to the create body and validated by the same validateWorkflowSpec \u2014 this is a FULL REPLACE, not a merge: every omitted field reverts to its default (e.g. omitting `active` re-activates, omitting `entry_conditions` clears them). PATCH is accepted on this path and behaves identically. `version` is incremented server-side; it cannot be set.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "slug": {
                    "type": "string"
                  },
                  "active": {
                    "type": "boolean",
                    "default": true
                  },
                  "entry_dataset": {
                    "type": "string",
                    "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$"
                  },
                  "entry_conditions": {
                    "type": "array",
                    "default": [],
                    "items": {
                      "type": "object",
                      "required": [
                        "path"
                      ],
                      "additionalProperties": false,
                      "description": "One condition dialect (#165): {path, op, value}, or {path, equals} as shorthand for op \"equals\". Refused at save: both forms at once, a missing value (except exists), gt/lt with a non-number, contains with a non-string, an object or array value, an unknown key. A {{placeholder}} value is refused here: nothing resolves it, so it would be compared as literal text.",
                      "properties": {
                        "path": {
                          "type": "string",
                          "minLength": 1,
                          "description": "Dot path. The empty path is rejected."
                        },
                        "op": {
                          "type": "string",
                          "enum": [
                            "equals",
                            "not_equals",
                            "contains",
                            "exists",
                            "gt",
                            "lt"
                          ]
                        },
                        "value": {
                          "description": "equals/not_equals compare with strict ===/!== (5 and \"5\" differ); contains needs a string; gt/lt need a number; exists takes none."
                        },
                        "equals": {
                          "description": "Shorthand for op \"equals\" with this value. Not together with op or value."
                        }
                      }
                    }
                  },
                  "steps": {
                    "type": "array",
                    "minItems": 1,
                    "description": "Same nine step types and same limits as createWorkflow (<= 32 steps including branch sub-steps; branch depth <= 3; no delay/wait_for_event inside a branch; delay and wait timeout <= 365 days; steps <= 262144 bytes serialized).",
                    "items": {
                      "type": "object"
                    }
                  }
                },
                "minProperties": 1
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          }
        ],
        "description": "Like PUT, but what is not sent keeps its stored value: {\"name\": ...} renames it and {\"active\": false} stops it starting new runs, without resending the steps. The merged definition is validated as on create. The version goes up only when the steps, entry dataset or entry conditions change."
      },
      "delete": {
        "operationId": "deleteWorkflow",
        "summary": "Deactivate, or permanently delete, a workflow",
        "description": "By default the workflow is DEACTIVATED: `active` is set to 0, the definition and all its run history are kept, it stays in listWorkflows, and PATCH {\"active\": true} turns it back on. Runs already in progress carry on. With ?permanent=true it is deleted for good (#256): the definition (a workflow's version is a number on it, not a separate row) and its whole run history, meaning every run, its step log and any legacy-engine waiter. Nothing else goes: records and deliveries the runs produced, and the AI call log, are kept. A permanent delete is refused (409) while any run has not finished: pending, running, waiting, or paused by a workspace suspension. Cancel those runs first (POST workflow-instances/{id}/cancel). The workflow is deactivated before its history is deleted, so it starts no new runs meanwhile. The history goes in chunks, at most 10,000 runs a call; a longer one answers 202 with `done: false`, and the same DELETE again carries on until the one that removes the workflow answers 200. Each call that deletes something writes a `delete_workflow` audit row (details: permanent, done, name, project_id, runs_deleted, step_events_deleted); a deactivation writes one with `permanent: false`. Scoped to the caller's workspace and this project: a workflow of another project or workspace is not found.",
        "tags": [
          "Workflows"
        ],
        "responses": {
          "200": {
            "description": "Deactivated (`{\"ok\": true}`), or with ?permanent=true deleted along with its runs and their step log (`done: true`, and what went in `deleted`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "done": {
                      "type": "boolean",
                      "enum": [
                        true
                      ],
                      "description": "Permanent delete only: the workflow itself is gone."
                    },
                    "deleted": {
                      "type": "object",
                      "required": [
                        "runs",
                        "step_events"
                      ],
                      "properties": {
                        "runs": {
                          "type": "integer",
                          "description": "Runs this call deleted."
                        },
                        "step_events": {
                          "type": "integer",
                          "description": "Step-log rows this call deleted with them."
                        }
                      },
                      "description": "Permanent delete only: what this call deleted."
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Permanent delete only: the run history is longer than one call deletes (10,000 runs). This call deleted `deleted`; `remaining_runs` are left, and the workflow is still listed, now inactive. Send the same DELETE again to carry on.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "done",
                    "deleted",
                    "remaining_runs",
                    "message"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "done": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "deleted": {
                      "type": "object",
                      "required": [
                        "runs",
                        "step_events"
                      ],
                      "properties": {
                        "runs": {
                          "type": "integer",
                          "description": "Runs this call deleted."
                        },
                        "step_events": {
                          "type": "integer",
                          "description": "Step-log rows this call deleted with them."
                        }
                      }
                    },
                    "remaining_runs": {
                      "type": "integer",
                      "description": "Runs of the workflow still to delete."
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`permanent must be true or false`: the query parameter had another value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Workflow not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Permanent delete only: a run of this workflow has not finished (pending, running, waiting, or paused by a suspension), so nothing was changed. `error` says to cancel those runs first (and to deactivate the workflow, if it is on) and then names up to 10 of them by id, so an agent that sees only the message can cancel them; `instances` lists the same runs, oldest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error",
                    "code",
                    "unfinished_runs",
                    "instances"
                  ],
                  "properties": {
                    "error": {
                      "type": "string",
                      "description": "Human-readable reason, naming up to 10 unfinished runs."
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "unfinished_runs"
                      ]
                    },
                    "unfinished_runs": {
                      "type": "integer",
                      "description": "How many runs have not finished; may be more than are listed."
                    },
                    "instances": {
                      "type": "array",
                      "maxItems": 10,
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "state",
                          "paused"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "The run (workflow instance) id, as cancelWorkflowInstance takes it."
                          },
                          "state": {
                            "type": "string",
                            "description": "Its state: pending, running or waiting."
                          },
                          "paused": {
                            "type": "boolean",
                            "description": "True when a workspace suspension paused it."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "permanent",
            "in": "query",
            "required": false,
            "description": "true deletes the workflow and its run history for good. Omitted or false, the workflow is only deactivated (active set to 0), and PATCH active:true brings it back. Any other value is refused with 400.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/workflows/{workflow_id}/instances": {
      "get": {
        "operationId": "listWorkflowInstances",
        "summary": "List instances of one workflow",
        "tags": [
          "Workflows"
        ],
        "responses": {
          "200": {
            "description": "Instances, newest first. Page with `limit`/`offset`; `total` counts every instance matching the filters. `context` is not included here \u2014 use getWorkflowInstance.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "instances"
                  ],
                  "properties": {
                    "instances": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "state": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "running",
                              "waiting",
                              "completed",
                              "failed",
                              "timed_out",
                              "cancelled"
                            ]
                          },
                          "current_step_index": {
                            "type": "integer"
                          },
                          "correlation_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "last_error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "updated_at": {
                            "type": "string"
                          },
                          "engine": {
                            "type": "string",
                            "enum": [
                              "legacy",
                              "cf"
                            ]
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Instances matching the filters, across all pages."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`. An unknown workflow id is NOT a 404 here \u2014 it returns an empty list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`Method not allowed` \u2014 this sub-path is GET-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "`since` or `until` is not an ISO 8601 timestamp.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "workflow_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Exact-match state filter. Unrecognized values simply return an empty list.",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "running",
                "waiting",
                "completed",
                "failed",
                "timed_out",
                "cancelled"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Only instances created at or after this instant (ISO 8601). Unparseable \u2192 400.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "Only instances created at or before this instant (ISO 8601). Unparseable \u2192 400.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1\u2013500 (default 50 when paging). With neither limit nor offset the answer is the newest 500.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip, newest first.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/workflow-instances/{instance_id}": {
      "get": {
        "operationId": "getWorkflowInstance",
        "summary": "Get a workflow instance with its step events and per-step status",
        "tags": [
          "Workflows"
        ],
        "responses": {
          "200": {
            "description": "Instance detail, its oldest-first step-event log, the engine it runs on, per-step status with what each step did, and the record that started the run.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "instance",
                    "events",
                    "engine",
                    "steps",
                    "trigger",
                    "cf_status"
                  ],
                  "properties": {
                    "instance": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "workflow_id": {
                          "type": "string"
                        },
                        "state": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "running",
                            "waiting",
                            "completed",
                            "failed",
                            "timed_out",
                            "cancelled"
                          ]
                        },
                        "current_step_index": {
                          "type": "integer"
                        },
                        "context": {
                          "type": "string",
                          "description": "JSON string of the instance scratchpad (entry payload plus each step's output_key), redacted with the same rules as the step samples (#247): a credential-named key's value, a Bearer/Basic value and a URL password read \"[redacted]\"; nothing else is cut. The stored run state is not redacted (the run needs it); stored text is truncated to 100000 chars on write, and a truncated context that no longer parses is withheld as a JSON string saying so."
                        },
                        "correlation_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "last_error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "created_at": {
                          "type": "string"
                        },
                        "updated_at": {
                          "type": "string"
                        },
                        "engine": {
                          "type": "string",
                          "enum": [
                            "legacy",
                            "cf"
                          ],
                          "description": "`cf`: runs on Cloudflare Workflows (every instance started since #130). `legacy`: started earlier, finishing on the original engine."
                        },
                        "cf_instance_id": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The Cloudflare Workflows instance id: `hk_` + SHA-256 of (workflow id, entry record id), so one event can never start the same workflow twice."
                        },
                        "run_attempt": {
                          "type": "integer",
                          "description": "1, plus one per retry."
                        },
                        "workflow_version": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "The definition version the run loaded."
                        }
                      }
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "step_index": {
                            "type": "integer"
                          },
                          "step_type": {
                            "type": "string",
                            "enum": [
                              "emit_event",
                              "call_ai",
                              "wait_for_event",
                              "delay",
                              "agent_call",
                              "branch",
                              "http",
                              "set",
                              "transform"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "started",
                              "waiting",
                              "completed",
                              "failed",
                              "skipped"
                            ]
                          },
                          "details": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Raw JSON string, at most 4000 chars and always valid JSON (samples are dropped before anything is cut). Rows carry `path` (and on the Cloudflare engine `attempt`); a branch row carries `taken`; a completed step carries what it did (see steps[].details)."
                          },
                          "created_at": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "engine": {
                      "type": "string",
                      "enum": [
                        "legacy",
                        "cf"
                      ]
                    },
                    "steps": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "index",
                          "path",
                          "type",
                          "status",
                          "started_at",
                          "finished_at",
                          "error",
                          "details"
                        ],
                        "properties": {
                          "index": {
                            "type": "integer",
                            "description": "Top-level step index (a branch sub-step carries its branch's index)."
                          },
                          "path": {
                            "type": "string",
                            "description": "\"2\" for a top-level step, \"2.then[0]\" / \"2.else[1]\" inside a branch."
                          },
                          "type": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "completed",
                              "running",
                              "waiting",
                              "failed",
                              "skipped"
                            ],
                            "description": "skipped = a sub-step of the branch arm not taken."
                          },
                          "started_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "finished_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "details": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "What the step log says about the step (#196, FLOW-15), from its latest event with details. By step: branch `taken` (then|else) and `sub_steps`; emit_event `dataset`, `record_id`, `delivered`; call_ai `model`, `input_tokens`, `output_tokens`, `latency_ms`, `output_type`, `output_key`; agent_call `agent_id`, `model`, `memory_recalled`; http `method`, `url` (no credentials, query values replaced by \u2026), `status`; set `keys`; failed steps `error`. `input` and `output` are SAMPLES: strings over 200 characters are cut, arrays keep 10 items, objects 20 keys, depth 4, and a sample over 1,200 characters becomes {truncated, preview}; a credential-named key (password, token, secret, api key, authorization, cookie, signature\u2026), a Bearer/Basic value and a URL password are replaced by \"[redacted]\". Request headers and bodies are never logged.",
                            "additionalProperties": true
                          }
                        }
                      }
                    },
                    "cf_status": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "Cloudflare's own instance status; null for a legacy instance or when Cloudflare no longer holds it (past retention).",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "queued",
                            "running",
                            "paused",
                            "errored",
                            "terminated",
                            "complete",
                            "waiting",
                            "waitingForPause",
                            "unknown"
                          ]
                        },
                        "error": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "message": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "trigger": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "The record that started the run (the instance's correlation_id), looked up in this project only. Null when it has been pruned by retention or is in another project.",
                      "required": [
                        "record_id",
                        "dataset",
                        "received_at",
                        "source"
                      ],
                      "properties": {
                        "record_id": {
                          "type": "string"
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "received_at": {
                          "type": "string"
                        },
                        "source": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Instance not found` (including an instance of another project or workspace), `Instance id required` (no id segment), or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instance_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "For a Cloudflare Workflows instance (engine `cf`) the API asks Cloudflare for the instance's status (`cf_status`), and a row that missed the end of its run is brought into line with it. `steps` is derived from the step log of the current attempt, one entry per step path, for overlaying run status on the workflow graph."
      }
    },
    "/admin/api/projects/{project_id}/workflow-instances/{instance_id}/cancel": {
      "post": {
        "operationId": "cancelWorkflowInstance",
        "summary": "Cancel a workflow instance",
        "description": "Cloudflare engine: terminates the Cloudflare Workflows instance, then marks the row cancelled and clears its wait. Legacy engine: marks it cancelled and removes its waiters, so the sweep never resumes it. Audited as cancel_workflow_instance.",
        "tags": [
          "Workflows"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instance_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled. `state` is `cancelled`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "state"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "state": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (write capability required), or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Instance not found` (including an instance of another project or workspace), or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`Instance is already <state>` \u2014 completed, failed, cancelled or timed_out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Cloudflare refused the operation; nothing was changed. Try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`Cloudflare Workflows is not configured` (a Cloudflare instance on a Worker without the WORKFLOW binding).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/workflow-instances/{instance_id}/retry": {
      "post": {
        "operationId": "retryWorkflowInstance",
        "summary": "Retry a workflow instance",
        "description": "Cloudflare engine (failed, timed_out or cancelled): restart() runs the instance again from the first step under a new run_attempt, with the context reset to the entry record; records an earlier attempt emitted are not emitted again. Legacy engine (failed or timed_out): continues from the step that failed (a timed-out wait parks again). Audited as retry_workflow_instance.",
        "tags": [
          "Workflows"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instance_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Retried. `state` is `pending` for a Cloudflare instance (it runs asynchronously); for a legacy instance, the state after it ran as far as it could.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "state"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "state": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (write capability required), or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Instance not found` (including an instance of another project or workspace), or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`A <state> instance cannot be retried (only failed, timed_out, cancelled)` \u2014 or, for a legacy instance, only failed and timed_out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Cloudflare refused the operation; nothing was changed. Try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`Cloudflare Workflows is not configured` (a Cloudflare instance on a Worker without the WORKFLOW binding).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/ai-agents": {
      "get": {
        "operationId": "listAiAgents",
        "summary": "List AI agents in a project",
        "tags": [
          "AI agents"
        ],
        "responses": {
          "200": {
            "description": "Agents, newest first. Soft-deleted agents (active = 0) are still listed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ai_agents"
                  ],
                  "properties": {
                    "ai_agents": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "active": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ]
                          },
                          "model": {
                            "type": "string"
                          },
                          "system_prompt": {
                            "type": "string"
                          },
                          "tools": {
                            "type": "string",
                            "description": "Raw JSON string array. Reserved for tool-calling; unused today."
                          },
                          "memory_dataset": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "max_tokens": {
                            "type": "integer"
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "updated_at": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "Pages with ?limit and ?offset and returns total, limit and offset beside the array, which is unchanged (DX-6, #201)."
      },
      "post": {
        "operationId": "createAiAgent",
        "summary": "Create an AI agent",
        "tags": [
          "AI agents"
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "ok"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "ai_agent": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "active": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "model": {
                          "type": "string"
                        },
                        "system_prompt": {
                          "type": "string"
                        },
                        "tools": {
                          "type": "string",
                          "description": "Raw JSON string array. Reserved for tool-calling; unused today."
                        },
                        "memory_dataset": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "max_tokens": {
                          "type": "integer"
                        },
                        "created_at": {
                          "type": "string"
                        },
                        "updated_at": {
                          "type": "string"
                        }
                      },
                      "description": "The created ai agent, as GET by id returns it (#201)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`body must be an object`; `name is required (<=120 chars)`; `model \"\u2026\" is not a supported model. \u2026`; `system_prompt too long (<=8000 chars)`; `tools must be an array`; `tools are not supported: \u2026`; `memory_dataset: <dataset name error>`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (write capability required), or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120,
                    "description": "Trimmed before validation."
                  },
                  "active": {
                    "type": "boolean",
                    "default": true,
                    "description": "Only the literal false deactivates. An inactive agent makes an agent_call workflow step fail."
                  },
                  "model": {
                    "type": "string",
                    "enum": [
                      "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
                      "@cf/meta/llama-3.1-8b-instruct",
                      "@cf/meta/llama-3.1-8b-instruct-fast",
                      "@cf/meta/llama-3.2-3b-instruct",
                      "@cf/meta/llama-4-scout-17b-16e-instruct",
                      "@cf/mistralai/mistral-small-3.1-24b-instruct",
                      "@cf/google/gemma-3-12b-it"
                    ],
                    "default": "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
                    "description": "A model from GET /admin/api/ai-models. Absent or empty means the default (@cf/meta/llama-3.3-70b-instruct-fp8-fast); anything off the list is refused with 400 `\u2026 is not a supported model` (#196, FLOW-16). It used to be replaced by the default without a word."
                  },
                  "system_prompt": {
                    "type": "string",
                    "maxLength": 8000,
                    "default": "",
                    "description": "`prompt` is accepted as an alias."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "Alias for system_prompt."
                  },
                  "tools": {
                    "type": "array",
                    "maxItems": 0,
                    "items": {},
                    "description": "Not supported: an agent makes one model call and cannot call tools. An empty list is accepted; a non-empty one is refused with 400 (#196). It used to be stored and ignored."
                  },
                  "memory_dataset": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "pattern": "^[a-zA-Z][a-zA-Z0-9_]{0,62}$",
                    "description": "When set, each output is written there as a record, and the agent's latest 5 outputs there (its own, this project's, oldest first, at most 2,000 characters) are sent with its next call (#196)."
                  },
                  "max_tokens": {
                    "type": "integer",
                    "default": 1024,
                    "description": "Clamped to 1..1024; a non-integer becomes 1024."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/ai-agents/{agent_id}": {
      "get": {
        "operationId": "getAiAgent",
        "summary": "Get one AI agent",
        "tags": [
          "AI agents"
        ],
        "responses": {
          "200": {
            "description": "The agent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ai_agent"
                  ],
                  "properties": {
                    "ai_agent": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "active": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "model": {
                          "type": "string"
                        },
                        "system_prompt": {
                          "type": "string"
                        },
                        "tools": {
                          "type": "string"
                        },
                        "memory_dataset": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "max_tokens": {
                          "type": "integer"
                        },
                        "created_at": {
                          "type": "string"
                        },
                        "updated_at": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Agent not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "agent_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "put": {
        "operationId": "updateAiAgent",
        "summary": "Replace an AI agent",
        "tags": [
          "AI agents"
        ],
        "responses": {
          "200": {
            "description": "Replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Same validation messages as createAiAgent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Agent not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "agent_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "description": "Identical to the create body and validated by the same validateAgentSpec \u2014 FULL REPLACE, not a merge: an omitted field reverts to its default (omitting `system_prompt` blanks it, omitting `tools` empties it, omitting `max_tokens` resets it to 1024, omitting `active` re-activates). PATCH is accepted on this path and behaves identically.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "active": {
                    "type": "boolean",
                    "default": true
                  },
                  "model": {
                    "type": "string",
                    "enum": [
                      "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
                      "@cf/meta/llama-3.1-8b-instruct",
                      "@cf/meta/llama-3.1-8b-instruct-fast",
                      "@cf/meta/llama-3.2-3b-instruct",
                      "@cf/meta/llama-4-scout-17b-16e-instruct",
                      "@cf/mistralai/mistral-small-3.1-24b-instruct",
                      "@cf/google/gemma-3-12b-it"
                    ],
                    "default": "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
                    "description": "As for create: a model off the list is refused."
                  },
                  "system_prompt": {
                    "type": "string",
                    "maxLength": 8000
                  },
                  "prompt": {
                    "type": "string"
                  },
                  "tools": {
                    "type": "array",
                    "maxItems": 0,
                    "items": {},
                    "description": "As for create: not supported; only an empty list is accepted."
                  },
                  "memory_dataset": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "max_tokens": {
                    "type": "integer",
                    "default": 1024
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "operationId": "patchAiAgent",
        "summary": "Change an AI agent (only the fields sent)",
        "tags": [
          "AI agents"
        ],
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`body must be an object`, or the createAiAgent validation messages, applied to the agent as it will be after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Agent not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "agent_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Any subset of the create body. Fields left out keep their current values \u2014 unlike PUT, which replaces the whole agent and resets what it omits. `memory_dataset: null` stops storing replies; `active: false` retires it. The MCP tool update_ai_agent uses this.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "active": {
                    "type": "boolean",
                    "default": true
                  },
                  "model": {
                    "type": "string",
                    "description": "A model from GET /admin/api/ai-models; one off the list is refused. Left out, the stored model is kept, even one saved before the list existed (#196)."
                  },
                  "system_prompt": {
                    "type": "string",
                    "maxLength": 8000
                  },
                  "prompt": {
                    "type": "string"
                  },
                  "tools": {
                    "type": "array",
                    "maxItems": 0,
                    "items": {},
                    "description": "Not supported; only an empty list is accepted. Any tools stored before #196 (never used) are cleared by an edit."
                  },
                  "memory_dataset": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "max_tokens": {
                    "type": "integer",
                    "default": 1024
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "delete": {
        "operationId": "deleteAiAgent",
        "summary": "Retire, or permanently delete, an AI agent",
        "tags": [
          "AI agents"
        ],
        "responses": {
          "200": {
            "description": "By default the agent is retired - `active` set to 0, still listed, and an agent_call step naming it fails until it is reactivated. With ?permanent=true the row is removed and it leaves listAiAgents; a step naming it then fails with `not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Agent not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "agent_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "permanent",
            "in": "query",
            "required": false,
            "description": "true removes the agent for good. Omitted, the agent is retired: active is set to 0 and the definition kept, so PATCH active:true brings it back.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/portal": {
      "get": {
        "operationId": "getProjectPortal",
        "summary": "The project's one customer portal",
        "tags": [
          "Portals"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The project's customer portal, or null when none has been set up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "portal"
                  ],
                  "properties": {
                    "portal": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "event_types": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "branding": {
                          "type": "object",
                          "properties": {
                            "logo_url": {
                              "type": "string"
                            },
                            "primary_color": {
                              "type": "string"
                            }
                          }
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "embed_origins": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "put": {
        "operationId": "putProjectPortal",
        "summary": "Create or update the project's customer portal",
        "description": "Creates the portal on the first call and updates it after that. A project has exactly one portal; this is what the console's Settings \u2192 Customer portal pane saves.",
        "tags": [
          "Portals"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80,
                    "description": "Required when creating."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "A switched-off portal refuses its tokens and sessions and delivers nothing to its customers' destinations."
                  },
                  "event_types": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The project's datasets the portal exposes. Each must be a valid dataset name. An empty list exposes nothing: customers cannot add a destination, and existing ones receive nothing. Changes apply to every delivery from then on, including destinations created earlier."
                  },
                  "branding": {
                    "type": "object",
                    "additionalProperties": false,
                    "description": "What the portal page applies (#198, PORT-12). Only these two keys; anything else is a 400. Omit to keep what is stored; `{}` clears it.",
                    "properties": {
                      "logo_url": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "An https image. The portal page's CSP allows images from this URL's origin and no other."
                      },
                      "primary_color": {
                        "type": "string",
                        "pattern": "^#[0-9a-fA-F]{6}$",
                        "description": "Stored lower-case. Button labels switch between black and white by contrast."
                      }
                    }
                  },
                  "embed_origins": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "description": "An exact https origin, e.g. https://app.example.com: no path, no wildcard. A trailing slash is removed."
                    },
                    "description": "The sites allowed to frame the portal page (#198, PORT-13): its CSP `frame-ancestors`. Empty (the default) means no site may. Omit to keep what is stored."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The project's customer portal, or null when none has been set up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "portal"
                  ],
                  "properties": {
                    "portal": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "event_types": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "branding": {
                          "type": "object",
                          "properties": {
                            "logo_url": {
                              "type": "string"
                            },
                            "primary_color": {
                              "type": "string"
                            }
                          }
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "embed_origins": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing name, a bad enabled value, or an invalid dataset name; or branding / embed_origins that fail validation."
          },
          "403": {
            "description": "Your role cannot change the portal. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A concurrent first save already created the project's portal."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "patchProjectPortal",
        "summary": "Create or update the project's customer portal (PATCH, same as PUT)",
        "description": "PATCH is accepted on this path and behaves exactly as PUT does: it creates the portal on the first call and updates it after that.",
        "tags": [
          "Portals"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80,
                    "description": "Required when creating."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "A switched-off portal refuses its tokens and sessions and delivers nothing to its customers' destinations."
                  },
                  "event_types": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The project's datasets the portal exposes. Each must be a valid dataset name. An empty list exposes nothing: customers cannot add a destination, and existing ones receive nothing. Changes apply to every delivery from then on, including destinations created earlier."
                  },
                  "branding": {
                    "type": "object",
                    "additionalProperties": false,
                    "description": "What the portal page applies (#198, PORT-12). Only these two keys; anything else is a 400. Omit to keep what is stored; `{}` clears it.",
                    "properties": {
                      "logo_url": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "An https image. The portal page's CSP allows images from this URL's origin and no other."
                      },
                      "primary_color": {
                        "type": "string",
                        "pattern": "^#[0-9a-fA-F]{6}$",
                        "description": "Stored lower-case. Button labels switch between black and white by contrast."
                      }
                    }
                  },
                  "embed_origins": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "description": "An exact https origin, e.g. https://app.example.com: no path, no wildcard. A trailing slash is removed."
                    },
                    "description": "The sites allowed to frame the portal page (#198, PORT-13): its CSP `frame-ancestors`. Empty (the default) means no site may. Omit to keep what is stored."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The project's customer portal, or null when none has been set up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "portal"
                  ],
                  "properties": {
                    "portal": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "event_types": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "branding": {
                          "type": "object",
                          "properties": {
                            "logo_url": {
                              "type": "string"
                            },
                            "primary_color": {
                              "type": "string"
                            }
                          }
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "embed_origins": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing name, a bad enabled value, or an invalid dataset name; or branding / embed_origins that fail validation."
          },
          "403": {
            "description": "Your role cannot change the portal. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A concurrent first save already created the project's portal."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "removeProjectPortal",
        "summary": "Remove the project's customer portal for good",
        "description": "Retires the portal: `retired_at` is set and it is switched off, every live access token is revoked, the pending deliveries (queued, retrying or paused) of its customers' destinations are cancelled, and one `remove_portal` audit row records it with counts. Nothing is deleted: the portal row, its customers' destinations and their delivery history stay. The project can then have a new portal. Unlike switching the portal off (`enabled: false`), this cannot be undone. The body must repeat the portal's name as `confirm`.",
        "tags": [
          "Portals"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirm"
                ],
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "The portal's current name, exactly."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The portal was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "portal"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "portal": {
                      "type": "null"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`confirm` is missing or is not the portal's name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Your role cannot remove the portal. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The project has no customer portal (or it was already removed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/portals": {
      "get": {
        "operationId": "listPortals",
        "summary": "List customer portals in a project",
        "tags": [
          "Portals"
        ],
        "responses": {
          "200": {
            "description": "Portals, newest first. Disabled portals (enabled = 0) are still listed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "portals"
                  ],
                  "properties": {
                    "portals": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "slug": {
                            "type": "string"
                          },
                          "event_types": {
                            "type": "string",
                            "description": "Raw JSON string array of dataset names exposed to the portal's customers."
                          },
                          "branding": {
                            "type": "object",
                            "properties": {
                              "logo_url": {
                                "type": "string"
                              },
                              "primary_color": {
                                "type": "string"
                              }
                            }
                          },
                          "enabled": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ]
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "updated_at": {
                            "type": "string"
                          },
                          "embed_origins": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "operationId": "createPortal",
        "summary": "Create a customer portal",
        "tags": [
          "Portals"
        ],
        "responses": {
          "201": {
            "description": "Created and enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "slug",
                    "ok"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string",
                      "description": "The slug actually stored, after slugification."
                    },
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`name is required`; or branding / embed_origins that fail validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (write capability required), or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The project already has a customer portal (one per project), or the slug is taken in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Trimmed; must be non-empty."
                  },
                  "slug": {
                    "type": "string",
                    "description": "Slugified server-side (lowercase, [a-z0-9-], max 48, falling back to 'portal'). Defaults to a slug of `name`. Unique per workspace."
                  },
                  "event_types": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "default": [],
                    "description": "Dataset names the portal exposes. Non-string entries are dropped. This also bounds what a portal customer may filter their own destinations to."
                  },
                  "branding": {
                    "type": "object",
                    "additionalProperties": false,
                    "description": "What the portal page applies (#198, PORT-12). Only these two keys; anything else is a 400. Omit to keep what is stored; `{}` clears it.",
                    "properties": {
                      "logo_url": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "An https image. The portal page's CSP allows images from this URL's origin and no other."
                      },
                      "primary_color": {
                        "type": "string",
                        "pattern": "^#[0-9a-fA-F]{6}$",
                        "description": "Stored lower-case. Button labels switch between black and white by contrast."
                      }
                    }
                  },
                  "embed_origins": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "description": "An exact https origin, e.g. https://app.example.com: no path, no wildcard. A trailing slash is removed."
                    },
                    "description": "The sites allowed to frame the portal page (#198, PORT-13): its CSP `frame-ancestors`. Empty (the default) means no site may. Omit to keep what is stored."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/portals/{portal_id}": {
      "get": {
        "operationId": "getPortal",
        "summary": "Get one customer portal",
        "tags": [
          "Portals"
        ],
        "responses": {
          "200": {
            "description": "The portal.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "portal"
                  ],
                  "properties": {
                    "portal": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "event_types": {
                          "type": "string"
                        },
                        "branding": {
                          "type": "object",
                          "properties": {
                            "logo_url": {
                              "type": "string"
                            },
                            "primary_color": {
                              "type": "string"
                            }
                          }
                        },
                        "enabled": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ]
                        },
                        "created_at": {
                          "type": "string"
                        },
                        "updated_at": {
                          "type": "string"
                        },
                        "embed_origins": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Portal not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "portal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "put": {
        "operationId": "updatePortal",
        "summary": "Update a customer portal",
        "tags": [
          "Portals"
        ],
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Portal not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "`enabled must be true or false`, or an invalid dataset name in `event_types`; or branding / embed_origins that fail validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "portal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "PATCH is accepted on this path and behaves identically. `slug` cannot be changed. Only the fields sent change; an omitted field keeps its current value.",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Trimmed. Omitted or blank leaves the existing name unchanged."
                  },
                  "event_types": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Replaces the exposed datasets when sent; omitted keeps them. An empty list exposes nothing: the portal's customers receive nothing."
                  },
                  "branding": {
                    "type": "object",
                    "additionalProperties": false,
                    "description": "What the portal page applies (#198, PORT-12). Only these two keys; anything else is a 400. Omit to keep what is stored; `{}` clears it.",
                    "properties": {
                      "logo_url": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "An https image. The portal page's CSP allows images from this URL's origin and no other."
                      },
                      "primary_color": {
                        "type": "string",
                        "pattern": "^#[0-9a-fA-F]{6}$",
                        "description": "Stored lower-case. Button labels switch between black and white by contrast."
                      }
                    }
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "true or false; omitted keeps the current state. A switched-off portal refuses its tokens and delivers nothing to its customers' destinations."
                  },
                  "embed_origins": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "description": "An exact https origin, e.g. https://app.example.com: no path, no wildcard. A trailing slash is removed."
                    },
                    "description": "The sites allowed to frame the portal page (#198, PORT-13): its CSP `frame-ancestors`. Empty (the default) means no site may. Omit to keep what is stored."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "operationId": "patchPortal",
        "summary": "Update a customer portal (PATCH, same as PUT)",
        "description": "PATCH is accepted on this path and behaves exactly as PUT does: only the fields sent change, and `slug` cannot be changed.",
        "tags": [
          "Portals"
        ],
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Portal not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "`enabled must be true or false`, or an invalid dataset name in `event_types`; or branding / embed_origins that fail validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "portal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "PATCH is accepted on this path and behaves identically. `slug` cannot be changed. Only the fields sent change; an omitted field keeps its current value.",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Trimmed. Omitted or blank leaves the existing name unchanged."
                  },
                  "event_types": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Replaces the exposed datasets when sent; omitted keeps them. An empty list exposes nothing: the portal's customers receive nothing."
                  },
                  "branding": {
                    "type": "object",
                    "additionalProperties": false,
                    "description": "What the portal page applies (#198, PORT-12). Only these two keys; anything else is a 400. Omit to keep what is stored; `{}` clears it.",
                    "properties": {
                      "logo_url": {
                        "type": "string",
                        "format": "uri",
                        "maxLength": 2048,
                        "description": "An https image. The portal page's CSP allows images from this URL's origin and no other."
                      },
                      "primary_color": {
                        "type": "string",
                        "pattern": "^#[0-9a-fA-F]{6}$",
                        "description": "Stored lower-case. Button labels switch between black and white by contrast."
                      }
                    }
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "true or false; omitted keeps the current state. A switched-off portal refuses its tokens and delivers nothing to its customers' destinations."
                  },
                  "embed_origins": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "type": "string",
                      "description": "An exact https origin, e.g. https://app.example.com: no path, no wildcard. A trailing slash is removed."
                    },
                    "description": "The sites allowed to frame the portal page (#198, PORT-13): its CSP `frame-ancestors`. Empty (the default) means no site may. Omit to keep what is stored."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "delete": {
        "operationId": "deletePortal",
        "summary": "Disable a customer portal",
        "tags": [
          "Portals"
        ],
        "responses": {
          "200": {
            "description": "SOFT delete \u2014 `enabled` set to 0. The row and its tokens remain, but every portal token for it stops authenticating immediately.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Portal not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "portal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/portals/{portal_id}/tokens": {
      "get": {
        "operationId": "listPortalTokens",
        "summary": "List a portal's access tokens",
        "tags": [
          "Portals"
        ],
        "responses": {
          "200": {
            "description": "Tokens, newest first. Only the 12-character prefix is returned \u2014 tokens are stored SHA-256 hashed and the plaintext is shown once, at issuance. `customer` names the customer this portal serves (PORT-2g, #321): whoever holds a live token on it (not revoked, not expired) or still has an endpoint that is on. A token for anyone else is a 409. Null when the portal is free.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "tokens"
                  ],
                  "properties": {
                    "tokens": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "customer_id": {
                            "type": "string"
                          },
                          "token_prefix": {
                            "type": "string",
                            "description": "'hpt_' plus 8 hex characters."
                          },
                          "scopes": {
                            "type": "string",
                            "description": "Raw JSON string array. Stored but not currently enforced by the portal API."
                          },
                          "expires_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "revoked_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "customer_name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "The display name given at issuance, or null."
                          }
                        }
                      }
                    },
                    "customer": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "The one customer this portal serves, or null when it has none (PORT-2g). The console locks its token dialog to this id.",
                      "properties": {
                        "customer_id": {
                          "type": "string"
                        },
                        "customer_name": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The latest display name given at issuance, or null."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Portal not found`, or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "portal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "operationId": "issuePortalToken",
        "summary": "Issue or revoke a portal access token",
        "tags": [
          "Portals"
        ],
        "responses": {
          "200": {
            "description": "Revoke or offboard mode.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "destinations_disabled": {
                      "type": "integer",
                      "description": "How many of the customer's destinations were switched off."
                    },
                    "tokens_revoked": {
                      "type": "integer",
                      "description": "Offboard mode only."
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "destinations_disabled": 1
                }
              }
            }
          },
          "201": {
            "description": "Issue mode \u2014 the plaintext token is returned ONCE and is never retrievable again. Hand the customer `/portal/{workspace}/{portal_slug}#token=<token>`: in the fragment, the token never reaches a server or its logs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "token",
                    "expires_at"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "token": {
                      "type": "string",
                      "description": "'hpt_' plus 48 hex characters. Presented by the end customer as `Authorization: Bearer <token>` or a `?token=` query parameter."
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`customer_id is required` \u2014 issue mode with no non-empty customer_id and no `revoke`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (write capability required), or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Portal not found`, `Token not found` (revoke mode), or `Project not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`portal_customer_mismatch` (issue mode): this portal already serves another customer. The body names them in `customer_id` and `customer_name`. Issue the token for that customer, offboard them first (`{\"offboard\": customer_id}` on this route, or Offboard in the console), or give the new customer their own project and portal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "This portal already serves customer 'acme' (Acme Inc). A customer portal serves one customer, so its events never reach anyone else. Issue this token for 'acme', or offboard them first (Offboard customer in the console, or POST {\"offboard\": \"<customer_id>\"} to this route: it revokes their tokens and switches their endpoints off), or give the new customer their own project and portal.",
                  "reason": "portal_customer_mismatch",
                  "customer_id": "acme",
                  "customer_name": "Acme Inc"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "portal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "One endpoint, three modes, discriminated by the presence of a string `revoke` or `offboard`.",
                "oneOf": [
                  {
                    "title": "Issue",
                    "type": "object",
                    "required": [
                      "customer_id"
                    ],
                    "properties": {
                      "customer_id": {
                        "type": "string",
                        "minLength": 1,
                        "description": "The tenant's own identifier for their end customer. Trimmed; must be non-empty. Compared exactly (case-sensitive) with the portal's current customer: a different id is a 409 (PORT-2g)."
                      },
                      "scopes": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "default": [],
                        "description": "Stored on the token but not enforced by the portal API today."
                      },
                      "expires_in_days": {
                        "type": "number",
                        "description": "Must be finite and > 0 to take effect; anything else means the token never expires. The console defaults it to 30. A portal session made from the token never outlives it."
                      },
                      "customer_name": {
                        "type": "string",
                        "maxLength": 80,
                        "description": "Optional display name, shown to the customer in the portal instead of the id (#198). Trimmed; over 80 characters is a 400."
                      }
                    }
                  },
                  {
                    "title": "Revoke",
                    "type": "object",
                    "required": [
                      "revoke"
                    ],
                    "properties": {
                      "revoke": {
                        "type": "string",
                        "description": "Id of a token belonging to this portal. Sets revoked_at and writes a revoke_portal_token audit row (target: the token id; details: portal_id, customer_id) in the same batch; already-revoked tokens are treated as not found and write nothing. In the same batch, if that was the customer's last live token on this portal, every destination they created is switched off (offboarding, #198)."
                      }
                    }
                  },
                  {
                    "title": "Offboard",
                    "type": "object",
                    "required": [
                      "offboard"
                    ],
                    "properties": {
                      "offboard": {
                        "type": "string",
                        "description": "A customer_id on this portal. Revokes every token they hold and switches off every destination they created, in one batch, audited as offboard_portal_customer. A new token later brings them back with those destinations still off. Unknown customer: 404."
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "description": "**One customer per portal (PORT-2g, #321).** A portal's events are not scoped to a customer: every customer endpoint on a portal receives every event of each dataset the portal exposes. So a portal serves exactly one customer, and a vendor isolates customers by giving each one its own project and portal. Issuing a token whose `customer_id` differs from the portal's current customer (anyone holding a live token, not revoked and not expired, or still having an endpoint that is on) is a `409` with `reason: \"portal_customer_mismatch\"`. The check is made by the insert itself, so two concurrent issues for different customers cannot both succeed. The same customer can hold several tokens. Revoking a customer's last token or offboarding them switches their endpoints off and frees the portal for a new customer. Switching an offboarded customer's endpoint back on (`enabled: true` on the destination) is refused the same way while the portal serves someone else, and a portal customer's own create or enable re-checks their token inside the write (401 once it is revoked)."
      }
    },
    "/admin/api/agents": {
      "get": {
        "operationId": "listConnectedAgents",
        "summary": "List the signed-in user's OAuth-connected coding agents",
        "tags": [
          "Connected agents"
        ],
        "responses": {
          "200": {
            "description": "Grants for the CURRENT USER, across every workspace they belong to \u2014 this resource is scoped by user_id, never by tenant_id. Revoked grants are included (revoked_at set).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "agents",
                    "available_scopes",
                    "available_levels"
                  ],
                  "properties": {
                    "agents": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "client_id",
                          "name",
                          "scopes",
                          "connected_at",
                          "last_used_at",
                          "revoked_at",
                          "level"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Grant id \u2014 the handle for PATCH/DELETE."
                          },
                          "client_id": {
                            "type": "string",
                            "description": "The OAuth client id WorkOS issued to the agent."
                          },
                          "name": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Client name as registered, when known."
                          },
                          "scopes": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "hookie:read",
                                "hookie:write",
                                "hookie:manage"
                              ]
                            },
                            "description": "Every scope up to the grant's level (a grant is a ladder, so `hookie:manage` is stored with read and write). A newly seen agent starts read-only; the user widens it here."
                          },
                          "connected_at": {
                            "type": "string"
                          },
                          "last_used_at": {
                            "type": "string"
                          },
                          "revoked_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "level": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "read",
                              "write",
                              "manage",
                              null
                            ],
                            "description": "The rung the grant reaches \u2014 read < write < manage \u2014 or null when it holds no Hookie scope. A higher rung includes the lower ones."
                          }
                        }
                      }
                    },
                    "available_scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "hookie:read",
                        "hookie:write",
                        "hookie:manage"
                      ]
                    },
                    "available_levels": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "example": [
                        "read",
                        "write",
                        "manage"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Managing connected agents is not available to connected agents at any scope\u2026` with `code: \"agent_not_permitted\"` \u2014 the caller is itself an OAuth agent. An agent can never list, widen or revoke grants, including its own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/admin/api/agents/{grant_id}": {
      "patch": {
        "operationId": "updateConnectedAgentScopes",
        "summary": "Change what a connected agent may do",
        "tags": [
          "Connected agents"
        ],
        "responses": {
          "200": {
            "description": "Grant updated; effective on the agent's very next request (offline JWT verification is not consulted for scopes \u2014 this table is).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "scopes",
                    "level"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The scopes now stored: every scope up to the level."
                    },
                    "level": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "read",
                        "write",
                        "manage",
                        null
                      ],
                      "description": "The rung the grant reaches \u2014 read < write < manage \u2014 or null when it holds no Hookie scope. A higher rung includes the lower ones."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`level must be one of: read, write, manage`, `Send level (read, write or manage), or scopes as an array`, or `scopes must be a subset of: hookie:read, hookie:write, hookie:manage`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Managing connected agents is not available to connected agents at any scope\u2026` with `code: \"agent_not_permitted\"` (caller is an agent), or `Missing X-Requested-With header`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`No such connected agent` \u2014 the grant does not exist, belongs to another user, or has already been revoked. A revoked grant cannot be re-scoped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "grant_id",
            "in": "path",
            "required": true,
            "description": "Grant id from listConnectedAgents.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "level": {
                    "type": "string",
                    "enum": [
                      "read",
                      "write",
                      "manage"
                    ],
                    "description": "The rung to grant. Takes precedence over `scopes`."
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "hookie:read",
                        "hookie:write",
                        "hookie:manage"
                      ]
                    },
                    "description": "Read by its highest scope. Accepted for existing callers."
                  }
                },
                "example": {
                  "level": "write"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ],
        "description": "Set the grant to one rung of the ladder read < write < manage. Send `level` (what the console sends), or `scopes` \u2014 a list is read by its HIGHEST scope and stored as the ladder up to it, so `[\"hookie:manage\"]` stores all three. An empty `scopes` list leaves the agent connected with no access; to cut it off, revoke it."
      },
      "delete": {
        "operationId": "revokeConnectedAgent",
        "summary": "Revoke a connected agent",
        "tags": [
          "Connected agents"
        ],
        "responses": {
          "200": {
            "description": "Revoked. Immediate and per-agent: the agent's next request is rejected with 401 rather than waiting for its WorkOS token to expire.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Managing connected agents is not available to connected agents at any scope\u2026` with `code: \"agent_not_permitted\"` (caller is an agent), or `Missing X-Requested-With header`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`No such connected agent` \u2014 unknown grant, another user's grant, or already revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "grant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/admin/api/sso": {
      "get": {
        "operationId": "listSsoConfigs",
        "summary": "List workspace SSO configurations",
        "tags": [
          "SSO"
        ],
        "description": "The workspace's saved SSO configurations. Single sign-on is not available yet (SEC-13a, #257), so none of them is active: nobody signs in through a saved configuration, whatever its `enabled` says. Readable on every plan and by any role, so a configuration saved before then stays visible.",
        "responses": {
          "200": {
            "description": "Configurations, newest first, at most one per provider. `idp_certificate` is deliberately NOT selected, even though it is a public certificate.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "sso_configs"
                  ],
                  "properties": {
                    "sso_configs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "provider": {
                            "type": "string",
                            "enum": [
                              "saml",
                              "oidc"
                            ]
                          },
                          "enabled": {
                            "type": "integer",
                            "enum": [
                              0,
                              1
                            ],
                            "description": "The flag as it was saved. It has never switched anything on: there is no SSO sign-in."
                          },
                          "idp_entity_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "idp_sso_url": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "acs_url": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "allowed_domains": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Raw JSON string array of lowercased email domains."
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "updated_at": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "operationId": "createSsoConfig",
        "summary": "Create a workspace SSO configuration (not available)",
        "tags": [
          "SSO"
        ],
        "description": "Single sign-on is not available yet (SEC-13a, #257): there is no SAML or OIDC sign-in, so this always answers `501` with `reason: \"sso_not_available\"` and changes nothing. It answers before the plan, the role or the body is looked at. A configuration saved before then is kept as it was; list it with listSsoConfigs.",
        "responses": {
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Refused before the route is reached: the caller's email domain is not allowed, an agent bearer token carries no Hookie scope, `Missing X-Requested-With header` (a session without it), `reason: \"workspace_suspended\"` while the workspace is suspended, or an API key scoped to one project. Never a role or grant refusal: the 501 comes before the role is checked, so nobody is told to widen a grant for something no grant provides.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "`Single sign-on is not available yet, so an SSO configuration cannot be created or changed.` with `reason: \"sso_not_available\"` (SEC-13a, #257). There is no SAML or OIDC sign-in, so nothing may create or change a configuration, on any plan and for any role.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "required": [
                        "reason"
                      ],
                      "properties": {
                        "reason": {
                          "type": "string",
                          "enum": [
                            "sso_not_available"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/admin/api/sso/{sso_config_id}": {
      "put": {
        "operationId": "updateSsoConfig",
        "summary": "Replace a workspace SSO configuration (not available)",
        "tags": [
          "SSO"
        ],
        "description": "Single sign-on is not available yet (SEC-13a, #257): there is no SAML or OIDC sign-in, so this always answers `501` with `reason: \"sso_not_available\"` and changes nothing. It answers before the plan, the role or the body is looked at. A configuration saved before then is kept as it was; list it with listSsoConfigs.",
        "responses": {
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Refused before the route is reached: the caller's email domain is not allowed, an agent bearer token carries no Hookie scope, `Missing X-Requested-With header` (a session without it), `reason: \"workspace_suspended\"` while the workspace is suspended, or an API key scoped to one project. Never a role or grant refusal: the 501 comes before the role is checked, so nobody is told to widen a grant for something no grant provides.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "`Single sign-on is not available yet, so an SSO configuration cannot be created or changed.` with `reason: \"sso_not_available\"` (SEC-13a, #257). There is no SAML or OIDC sign-in, so nothing may create or change a configuration, on any plan and for any role.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "required": [
                        "reason"
                      ],
                      "properties": {
                        "reason": {
                          "type": "string",
                          "enum": [
                            "sso_not_available"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "sso_config_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "operationId": "patchSsoConfig",
        "summary": "Update a workspace SSO configuration (not available)",
        "tags": [
          "SSO"
        ],
        "description": "Single sign-on is not available yet (SEC-13a, #257): there is no SAML or OIDC sign-in, so this always answers `501` with `reason: \"sso_not_available\"` and changes nothing. It answers before the plan, the role or the body is looked at. A configuration saved before then is kept as it was; list it with listSsoConfigs.",
        "responses": {
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Refused before the route is reached: the caller's email domain is not allowed, an agent bearer token carries no Hookie scope, `Missing X-Requested-With header` (a session without it), `reason: \"workspace_suspended\"` while the workspace is suspended, or an API key scoped to one project. Never a role or grant refusal: the 501 comes before the role is checked, so nobody is told to widen a grant for something no grant provides.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "501": {
            "description": "`Single sign-on is not available yet, so an SSO configuration cannot be created or changed.` with `reason: \"sso_not_available\"` (SEC-13a, #257). There is no SAML or OIDC sign-in, so nothing may create or change a configuration, on any plan and for any role.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "required": [
                        "reason"
                      ],
                      "properties": {
                        "reason": {
                          "type": "string",
                          "enum": [
                            "sso_not_available"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "sso_config_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "delete": {
        "operationId": "deleteSsoConfig",
        "summary": "Delete a workspace SSO configuration",
        "tags": [
          "SSO"
        ],
        "responses": {
          "200": {
            "description": "Deleted (hard delete). The only way a saved configuration is removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 manage capability required, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`SSO config not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "sso_config_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/sso/{sso_config_id}/test": {
      "post": {
        "operationId": "testSsoConfig",
        "summary": "Check whether an SSO configuration is complete",
        "tags": [
          "SSO"
        ],
        "responses": {
          "200": {
            "description": "Completeness check only: it does NOT contact the IdP and does not attempt a login. Single sign-on is not available (#257), so nothing signs in through a configuration however complete it is, and `note` says so.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ready",
                    "missing",
                    "note"
                  ],
                  "properties": {
                    "ready": {
                      "type": "boolean",
                      "description": "True when all four of idp_entity_id, idp_sso_url, idp_certificate and acs_url are set."
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "idp_entity_id",
                          "idp_sso_url",
                          "idp_certificate",
                          "acs_url"
                        ]
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 manage capability required, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`SSO config not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`Method not allowed` \u2014 the test sub-path is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "name": "sso_config_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/ip-allowlist": {
      "patch": {
        "operationId": "updateIpAllowlist",
        "summary": "Replace the workspace-level ingest IP allowlist",
        "tags": [
          "IP allowlist"
        ],
        "responses": {
          "200": {
            "description": "Stored. The echoed list is the trimmed, validated set now in effect.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "ip_allowlist"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "ip_allowlist": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`Send ip_allowlist: an array of IPs or CIDRs, or [] to allow every address` (the field is required, even to clear), `ip_allowlist must be an array of CIDR strings`, `At most 64 CIDR entries are allowed`, or `Invalid CIDR/IP entry: <value>`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (owners and admins only: the manage capability), or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ip_allowlist": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "maxItems": 64,
                    "items": {
                      "type": "string",
                      "description": "A bare IPv4/IPv6 address or a CIDR block. Entries are trimmed."
                    },
                    "description": "Full replacement of the workspace allowlist. null, omitted, or an empty array CLEARS it (the column is set to NULL), which means every source IP is accepted again."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "put": {
        "operationId": "putIpAllowlist",
        "summary": "Replace the workspace-level ingest IP allowlist",
        "tags": [
          "IP allowlist"
        ],
        "responses": {
          "200": {
            "description": "Stored. The echoed list is the trimmed, validated set now in effect.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "ip_allowlist"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "ip_allowlist": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`Send ip_allowlist: an array of IPs or CIDRs, or [] to allow every address` (the field is required, even to clear), `ip_allowlist must be an array of CIDR strings`, `At most 64 CIDR entries are allowed`, or `Invalid CIDR/IP entry: <value>`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (owners and admins only: the manage capability), or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ip_allowlist": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "maxItems": 64,
                    "items": {
                      "type": "string",
                      "description": "A bare IPv4/IPv6 address or a CIDR block. Entries are trimmed."
                    },
                    "description": "Full replacement of the workspace allowlist. null, omitted, or an empty array CLEARS it (the column is set to NULL), which means every source IP is accepted again."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "get": {
        "operationId": "getIpAllowlist",
        "summary": "Read the workspace-level ingest IP allowlist",
        "tags": [
          "IP allowlist"
        ],
        "description": "Owners and admins only. A GET never changes the list (before SEC-4 it cleared it).",
        "responses": {
          "200": {
            "description": "The allowlist now in effect; empty means every address is accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ip_allowlist"
                  ],
                  "properties": {
                    "ip_allowlist": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (owners and admins only). For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/workspace": {
      "get": {
        "operationId": "getWorkspace",
        "summary": "The workspace's name, slug and former slugs",
        "tags": [
          "Workspace"
        ],
        "description": "Readable by any member.",
        "responses": {
          "200": {
            "description": "The workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "name",
                    "slug",
                    "aliases"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string",
                      "description": "The first path segment of every public endpoint URL: /{workspace}/{project}/{endpoint}."
                    },
                    "aliases": {
                      "type": "array",
                      "description": "Former slugs, newest first. Path-form ingest still accepts each one, permanently, so a URL a provider already holds keeps working. No other workspace can take them.",
                      "items": {
                        "type": "object",
                        "required": [
                          "slug",
                          "created_at"
                        ],
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string",
                            "description": "When the workspace stopped using it."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "operationId": "updateWorkspace",
        "summary": "Rename the workspace or change its slug",
        "tags": [
          "Workspace"
        ],
        "description": "The name needs the manage capability (owner or admin). The slug needs the OWNER: it is part of every public endpoint URL, so a connected agent, which is clipped to admin at most, can never change it. The slug given up becomes an alias that path-form ingest keeps accepting, and taking back one of the workspace's own former slugs makes it current again. Every change writes an `update_workspace` audit row with from and to.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80,
                    "description": "Trimmed. No control characters."
                  },
                  "slug": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 48,
                    "pattern": "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$",
                    "description": "Lowercase letters, digits and hyphens. Not one of the Worker's reserved first path segments (api, admin, next, brand, portal, mcp, cli, ...), and not another workspace's slug or former slug."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved (or nothing changed). The workspace as it now is.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "slug",
                        "aliases"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string",
                          "description": "The first path segment of every public endpoint URL: /{workspace}/{project}/{endpoint}."
                        },
                        "aliases": {
                          "type": "array",
                          "description": "Former slugs, newest first. Path-form ingest still accepts each one, permanently, so a URL a provider already holds keeps working. No other workspace can take them.",
                          "items": {
                            "type": "object",
                            "required": [
                              "slug",
                              "created_at"
                            ],
                            "properties": {
                              "slug": {
                                "type": "string"
                              },
                              "created_at": {
                                "type": "string",
                                "description": "When the workspace stopped using it."
                              }
                            }
                          }
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "ok"
                      ],
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`Nothing to update (name, slug)`, `name must be a non-empty string`, `name must be at most 80 characters`, `slug must be 3 to 48 characters`, `slug must be lowercase letters, digits and hyphens, starting and ending with a letter or digit`, or `\"<slug>\" is reserved`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (a rename needs owner or admin), `Only the workspace owner can change its slug: ...`, or `Missing X-Requested-With header`. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`That slug is already taken` (by another workspace, now or formerly), or `This workspace already keeps 20 former slugs working. ...` \u2014 a workspace keeps at most 20 former slugs; taking one back frees a place.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/billing/status": {
      "get": {
        "operationId": "getBillingStatus",
        "summary": "Current plan, subscription and display prices",
        "tags": [
          "Billing"
        ],
        "responses": {
          "200": {
            "description": "Readable by ANY workspace member \u2014 the owner-only gate starts after this branch.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "plan",
                    "subscription",
                    "prices",
                    "trial",
                    "portal_url"
                  ],
                  "properties": {
                    "plan": {
                      "type": "string",
                      "enum": [
                        "free",
                        "standard",
                        "team"
                      ],
                      "description": "'standard' is the internal id of the Pro plan."
                    },
                    "subscription": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Stripe subscription id."
                        },
                        "status": {
                          "type": "string",
                          "description": "Stripe subscription status as last seen by the webhook, e.g. active, trialing, past_due, unpaid, incomplete, paused, canceled. `trialing` is the paid plan in Stripe's trial: the card is first charged at current_period_end."
                        },
                        "plan": {
                          "type": "string",
                          "enum": [
                            "standard",
                            "team"
                          ]
                        },
                        "quantity": {
                          "type": "integer",
                          "description": "Seat count (Team is per seat)."
                        },
                        "current_period_end": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "cancel_at_period_end": {
                          "type": "integer",
                          "enum": [
                            0,
                            1
                          ],
                          "description": "1 while the subscription is cancelled and runs to the end of its period. Written by the Stripe webhook from customer.subscription.* events."
                        }
                      }
                    },
                    "prices": {
                      "type": "object",
                      "required": [
                        "pro",
                        "team"
                      ],
                      "properties": {
                        "pro": {
                          "type": "string",
                          "example": "9.99"
                        },
                        "team": {
                          "type": "string",
                          "example": "39.00"
                        }
                      },
                      "description": "Display only, overridable from platform_settings. What Stripe charges is fixed by the price ids."
                    },
                    "trial": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "ends_at": {
                          "type": "string"
                        }
                      }
                    },
                    "portal_url": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Stripe's hosted Customer Portal link. Non-null only on the standard/team plans and only when STRIPE_PORTAL_URL is configured. Cancel/update-payment-method happen there, not in this API."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/billing/subscribe": {
      "post": {
        "operationId": "createBillingCheckoutSession",
        "summary": "Start a new subscription via Stripe Checkout",
        "description": "Owner only. Starts a Stripe Checkout session for a NEW subscription; a workspace with a live one (active, trialing, past_due, unpaid, paused) is refused with 409. A workspace on a Hookie trial that checks out the plan it is trying keeps the rest of the trial: with at least 48 hours and ten minutes left, the session carries `subscription_data[trial_end]` = the trial's end (unix seconds, rounded down, so never later), the subscription starts in Stripe's trial, and the card is first charged when the trial ends. Stripe refuses a trial end less than 48 hours out, so closer than that, for any other plan, or with no trial, billing starts today.",
        "tags": [
          "Billing"
        ],
        "responses": {
          "200": {
            "description": "Checkout session created. Redirect the browser to `checkout_url`. Success/cancel return to {APP_ORIGIN}/?checkout=success|cancel#/settings/billing. The plan only changes when the signed Stripe webhook confirms the subscription.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "session_id"
                  ],
                  "properties": {
                    "session_id": {
                      "type": "string"
                    },
                    "checkout_url": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Stripe's hosted Checkout URL. Absent from Stripe's response in rare cases even when session_id is present."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Only the owner can manage billing` \u2014 billing is the one capability an OAuth agent can never reach at any scope. Also `Missing X-Requested-With header`. An agent \u2014 at any scope \u2014 is refused first, with `code: \"agent_not_permitted\"`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Unknown billing action` \u2014 a POST to /admin/api/billing/<anything else>.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`Use POST` \u2014 reached by any non-GET-status, non-POST request under /admin/api/billing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "`Billing is not configured` (no STRIPE_SECRET_KEY), or `No Pro plan configured` / `No Team plan configured` (the price id env var is unset).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`Stripe checkout session create failed`, or `Could not start subscription`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "team"
                    ],
                    "default": "standard",
                    "description": "Only the exact string 'team' selects Team; every other value, and an absent or unparseable body, means 'standard' (Pro). Team quantity is set server-side to the workspace's current membership count."
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/admin/api/billing/details": {
      "get": {
        "operationId": "getBillingDetails",
        "summary": "Renewal date, payment failures, a pending cancellation and invoices, from Stripe",
        "tags": [
          "Billing"
        ],
        "description": "Owner only (a connected agent is never the owner). Reads the workspace's subscription and its Stripe customer's 12 most recent invoices live from Stripe with the server's key, which never leaves the Worker. When Stripe is not configured or cannot be reached it answers with the state the webhook stored, `live: false`, and no invoices.",
        "responses": {
          "200": {
            "description": "The subscription as Plan & billing shows it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "plan",
                    "subscription",
                    "invoices",
                    "unpaid_invoice",
                    "live",
                    "stripe_error"
                  ],
                  "properties": {
                    "plan": {
                      "type": "string",
                      "enum": [
                        "free",
                        "standard",
                        "team"
                      ]
                    },
                    "subscription": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "description": "Stripe subscription status. `trialing` is the paid plan in Stripe's trial, first charged at renews_at."
                        },
                        "plan": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "quantity": {
                          "type": "integer"
                        },
                        "current_period_end": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "cancel_at_period_end": {
                          "type": "boolean"
                        },
                        "cancel_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "payment_failed": {
                          "type": "boolean",
                          "description": "true while the status is past_due or unpaid: a charge failed and Stripe is retrying, or has stopped."
                        },
                        "cancel_pending": {
                          "type": "boolean",
                          "description": "true when the subscription is cancelled but runs until ends_at."
                        },
                        "renews_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "When it next renews, or, while trialing, when the card is first charged; null once it is cancelling."
                        },
                        "ends_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "When a cancelled subscription ends."
                        }
                      }
                    },
                    "invoices": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "number",
                          "status",
                          "amount_due",
                          "amount_paid",
                          "currency",
                          "created",
                          "hosted_invoice_url",
                          "invoice_pdf"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "number": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Stripe invoice status: open, paid, void or uncollectible. Drafts are left out."
                          },
                          "amount_due": {
                            "type": "integer",
                            "description": "In the currency's minor unit (cents)."
                          },
                          "amount_paid": {
                            "type": "integer"
                          },
                          "currency": {
                            "type": "string",
                            "example": "usd"
                          },
                          "created": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "ISO 8601."
                          },
                          "hosted_invoice_url": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Stripe's hosted invoice page, where an open invoice can be paid. https only; anything else is null."
                          },
                          "invoice_pdf": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "unpaid_invoice": {
                      "oneOf": [
                        {
                          "type": "object",
                          "required": [
                            "id",
                            "number",
                            "status",
                            "amount_due",
                            "amount_paid",
                            "currency",
                            "created",
                            "hosted_invoice_url",
                            "invoice_pdf"
                          ],
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "number": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "status": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "Stripe invoice status: open, paid, void or uncollectible. Drafts are left out."
                            },
                            "amount_due": {
                              "type": "integer",
                              "description": "In the currency's minor unit (cents)."
                            },
                            "amount_paid": {
                              "type": "integer"
                            },
                            "currency": {
                              "type": "string",
                              "example": "usd"
                            },
                            "created": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "ISO 8601."
                            },
                            "hosted_invoice_url": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "description": "Stripe's hosted invoice page, where an open invoice can be paid. https only; anything else is null."
                            },
                            "invoice_pdf": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The newest open invoice with an amount still due: the one to pay after a failed payment."
                    },
                    "live": {
                      "type": "boolean",
                      "description": "false when this is the stored state because Stripe could not be asked."
                    },
                    "stripe_error": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Only the owner can manage billing`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/billing/portal": {
      "post": {
        "operationId": "createBillingPortalSession",
        "summary": "Open Stripe's Customer Portal for this workspace",
        "tags": [
          "Billing"
        ],
        "description": "Owner only. Creates a Stripe Billing Portal session for the workspace's Stripe customer, returning to Settings \u2192 Plan & billing, so the owner changes the card, cancels or downloads receipts without Stripe's email-login step. Falls back to the no-code portal login link (`session: false`) when the workspace has no Stripe customer yet or Stripe refuses.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "The URL to open.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url",
                    "session"
                  ],
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "session": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Only the owner can manage billing`, or `Missing X-Requested-With header`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`This workspace has no Stripe customer yet` (and no fallback link is configured).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "`Billing is not configured`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`Could not open the billing portal` (and no fallback link is configured).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/destination-presets": {
      "get": {
        "operationId": "listDestinationPresets",
        "summary": "Outbound destination preset catalog",
        "tags": [
          "Presets"
        ],
        "responses": {
          "200": {
            "description": "Static, read-only catalog compiled into the Worker \u2014 identical for every workspace, with no plan or role gate. Currently: slack, datadog, email, generic, all `available: true` since destinations carry headers, authentication and a body template (DLV-7). The slack preset has `type: \"slack\"`: it creates a Slack destination (the URL goes in config.webhook_url, encrypted) with no template, so it posts the readable message. Each preset carries the options its service needs \u2014 `transform` (the body template), `auth` (the kind of credential, never a value) and `headers` \u2014 for the console to prefill.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "presets"
                  ],
                  "properties": {
                    "presets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "description",
                          "url_hint",
                          "headers",
                          "transform",
                          "example_payload",
                          "docs",
                          "available"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "enum": [
                              "slack",
                              "datadog",
                              "email",
                              "generic"
                            ]
                          },
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "url_hint": {
                            "type": "string"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "webhook",
                              "slack"
                            ],
                            "description": "The destination type the preset creates (#313). Absent: a webhook, addressed by `url`. `slack`: create with type 'slack' and config.webhook_url."
                          },
                          "headers": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "string"
                            },
                            "description": "Non-secret custom headers the service wants. Content-Type is Hookie's and is not listed."
                          },
                          "example_payload": {
                            "type": "object",
                            "description": "What the body looks like once sent."
                          },
                          "docs": {
                            "type": "string",
                            "description": "Repository-relative documentation path, e.g. docs/integrations/slack.md."
                          },
                          "available": {
                            "type": "boolean",
                            "description": "Whether a destination made from this preset can deliver. All current presets can; the field stays for a future preset that needs something a destination lacks."
                          },
                          "unavailable_reason": {
                            "type": "string",
                            "description": "Present only when `available` is false: why."
                          },
                          "auth": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "required": [
                              "type",
                              "hint"
                            ],
                            "properties": {
                              "type": {
                                "type": "string",
                                "enum": [
                                  "bearer",
                                  "basic",
                                  "api_key"
                                ]
                              },
                              "header": {
                                "type": "string"
                              },
                              "hint": {
                                "type": "string",
                                "description": "Where to find the credential."
                              }
                            },
                            "description": "The authentication the service expects, without a credential."
                          },
                          "transform": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "description": "The body template for the service ({type:'template', template}), or null for Hookie's envelope."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/source-presets": {
      "get": {
        "operationId": "listSourcePresets",
        "summary": "Inbound source preset catalog",
        "tags": [
          "Presets"
        ],
        "responses": {
          "200": {
            "description": "Static, read-only catalog compiled into the Worker. Currently: stripe, github, shopify.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "presets"
                  ],
                  "properties": {
                    "presets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "provider",
                          "name",
                          "description",
                          "recommended_rules",
                          "verification_scheme",
                          "validation",
                          "docs"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "enum": [
                              "stripe",
                              "github",
                              "shopify"
                            ]
                          },
                          "provider": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "recommended_rules": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "name",
                                "conditions",
                                "dataset"
                              ],
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "conditions": {
                                  "type": "array",
                                  "items": {
                                    "$ref": "#/components/schemas/Condition"
                                  }
                                },
                                "dataset": {
                                  "type": "string"
                                }
                              }
                            }
                          },
                          "verification_scheme": {
                            "type": "string",
                            "enum": [
                              "none",
                              "stripe",
                              "github",
                              "shopify",
                              "slack",
                              "standard_webhooks",
                              "hmac"
                            ],
                            "description": "The endpoint verification scheme this provider signs with: pass it as verification.scheme, with the provider's secret, when creating the endpoint."
                          },
                          "validation": {
                            "type": "string",
                            "description": "How to turn on signature verification for this provider."
                          },
                          "docs": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/ai-models": {
      "get": {
        "operationId": "listAiModels",
        "summary": "Models an AI trigger, agent or workflow AI step may name",
        "tags": [
          "AI agents"
        ],
        "description": "The curated Workers AI text-generation models (#196, FLOW-16). AI triggers, AI agents and workflow call_ai steps refuse a model not on this list. Read-only; any member.",
        "responses": {
          "200": {
            "description": "The list and the default.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "models",
                    "default"
                  ],
                  "properties": {
                    "models": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "label"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "enum": [
                              "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
                              "@cf/meta/llama-3.1-8b-instruct",
                              "@cf/meta/llama-3.1-8b-instruct-fast",
                              "@cf/meta/llama-3.2-3b-instruct",
                              "@cf/meta/llama-4-scout-17b-16e-instruct",
                              "@cf/mistralai/mistral-small-3.1-24b-instruct",
                              "@cf/google/gemma-3-12b-it"
                            ]
                          },
                          "label": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "default": {
                      "type": "string",
                      "example": "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not permitted \u2014 the caller's email domain is not allowed, the role lacks this capability, or an agent bearer token carries no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`Method not allowed` \u2014 not a GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/v1/data": {
      "get": {
        "operationId": "listDataApiDatasets",
        "summary": "The datasets this key may read, and their columns",
        "tags": [
          "Data API"
        ],
        "security": [
          {
            "dataApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The project's exposed datasets.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "datasets"
                  ],
                  "properties": {
                    "datasets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "dataset": {
                            "type": "string"
                          },
                          "columns": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or revoked Data API key."
          },
          "403": {
            "description": "The key is valid but its workspace is suspended (`reason: \"workspace_suspended\"`). Until #173 this was a 401.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "The key's burst limit (the ingest limiter: about 100 requests per 10 seconds per key, a best-effort guard counted separately at each Cloudflare location, so a short burst can get through above it): {error: 'Too many requests for this key \u2014 slow down and retry', retry_after: 10}, with a Retry-After: 10 response header, the same value (#177). Fails open if the limiter is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer",
                          "enum": [
                            10
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 10. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/v1/data/{dataset}": {
      "get": {
        "operationId": "readDataApiDataset",
        "summary": "One page of an exposed dataset, exposed columns only",
        "tags": [
          "Data API"
        ],
        "security": [
          {
            "dataApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "dataset",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 25
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Only records received at or after this ISO 8601 time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "description": "Only records received before this ISO 8601 time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "csv for a UTF-8 CSV (with BOM, formula-safe) instead of JSON; the total is in X-Total-Count.",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "csv"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "dataset",
                    "columns",
                    "rows",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "dataset": {
                      "type": "string"
                    },
                    "columns": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true,
                        "description": "Only the exposed columns, keyed by column (a dotted column such as customer.tier is one key)."
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "since or until is not a timestamp."
          },
          "401": {
            "description": "Missing, invalid or revoked Data API key."
          },
          "403": {
            "description": "The key is valid but its workspace is suspended (`reason: \"workspace_suspended\"`). Until #173 this was a 401.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such dataset \u2014 or one this project does not expose; the two are deliberately indistinguishable."
          },
          "429": {
            "description": "The key's burst limit (the ingest limiter: about 100 requests per 10 seconds per key, a best-effort guard counted separately at each Cloudflare location, so a short burst can get through above it): {error: 'Too many requests for this key \u2014 slow down and retry', retry_after: 10}, with a Retry-After: 10 response header, the same value (#177). Fails open if the limiter is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer",
                          "enum": [
                            10
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 10. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/data-api": {
      "get": {
        "operationId": "getDataApi",
        "summary": "What the project exposes to the Data API, and its keys (metadata only)",
        "tags": [
          "Data API"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Exposures and keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exposures": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "dataset": {
                            "type": "string"
                          },
                          "columns": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "key_prefix": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "last_used_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "revoked_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Owner or admin only."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/data-api/exposures": {
      "put": {
        "operationId": "putDataApiExposures",
        "summary": "Replace what the project exposes",
        "tags": [
          "Data API"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "exposures"
                ],
                "properties": {
                  "exposures": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "dataset",
                        "columns"
                      ],
                      "properties": {
                        "dataset": {
                          "type": "string"
                        },
                        "columns": {
                          "type": "array",
                          "minItems": 1,
                          "maxItems": 100,
                          "items": {
                            "type": "string"
                          },
                          "description": "Top-level fields, dotted paths into nested objects, or _id / _received_at / _source."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The exposures now in force."
          },
          "400": {
            "description": "An invalid dataset name or column key, a duplicate dataset, or no columns."
          },
          "403": {
            "description": "Owner or admin only. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "patchDataApiExposures",
        "summary": "Replace what the project exposes (PATCH, same as PUT)",
        "description": "PATCH is accepted on this path and behaves exactly as PUT does: the list sent replaces the project's exposures.",
        "tags": [
          "Data API"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "exposures"
                ],
                "properties": {
                  "exposures": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "dataset",
                        "columns"
                      ],
                      "properties": {
                        "dataset": {
                          "type": "string"
                        },
                        "columns": {
                          "type": "array",
                          "minItems": 1,
                          "maxItems": 100,
                          "items": {
                            "type": "string"
                          },
                          "description": "Top-level fields, dotted paths into nested objects, or _id / _received_at / _source."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The exposures now in force."
          },
          "400": {
            "description": "An invalid dataset name or column key, a duplicate dataset, or no columns."
          },
          "403": {
            "description": "Owner or admin only. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/data-api/keys": {
      "post": {
        "operationId": "createDataApiKey",
        "summary": "Create a read-only Data API key (the plaintext is returned once)",
        "tags": [
          "Data API"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The key. Store it now; it is stored only as a hash.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string"
                    },
                    "key_prefix": {
                      "type": "string"
                    },
                    "created_at": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Owner or admin only. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/data-api/keys/{key_id}": {
      "delete": {
        "operationId": "revokeDataApiKey",
        "summary": "Revoke a Data API key; it stops working immediately",
        "tags": [
          "Data API"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked."
          },
          "403": {
            "description": "The workspace is suspended (`reason: \"workspace_suspended\"`): writes are refused until it is reinstated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such active key in this project."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/telemetry": {
      "get": {
        "operationId": "getBroadcastLogs",
        "summary": "The project's Broadcast Logs integration (or null) and the vendor catalog",
        "description": "Any member can read it. Secret values are never returned: `secrets_set` names the secret fields that are set.",
        "tags": [
          "Broadcast Logs"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The integration and the vendors the wizard offers, with each vendor's fields and OTLP docs link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "integration": {
                      "oneOf": [
                        {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "vendor": {
                              "type": "string",
                              "enum": [
                                "datadog",
                                "grafana_cloud",
                                "otel_collector",
                                "posthog",
                                "sentry"
                              ]
                            },
                            "vendor_name": {
                              "type": "string"
                            },
                            "signals": {
                              "type": "array",
                              "items": {
                                "type": "string",
                                "enum": [
                                  "logs",
                                  "traces"
                                ]
                              }
                            },
                            "settings": {
                              "type": "object",
                              "additionalProperties": {
                                "type": "string"
                              },
                              "description": "The vendor's non-secret settings (site, region, endpoint, instance id)."
                            },
                            "secrets_set": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              },
                              "description": "The NAMES of the secret fields that are set. Secret values are never returned."
                            },
                            "secrets_unreadable": {
                              "type": "boolean",
                              "description": "True when the stored credentials cannot be decrypted; nothing is sent until they are saved again."
                            },
                            "endpoints": {
                              "type": "object",
                              "properties": {
                                "logs": {
                                  "type": "string"
                                },
                                "traces": {
                                  "type": "string"
                                }
                              },
                              "description": "Where each signal is POSTed (URLs only; credentials travel in headers)."
                            },
                            "enabled": {
                              "type": "boolean"
                            },
                            "created_by": {
                              "type": "string"
                            },
                            "created_at": {
                              "type": "string"
                            },
                            "updated_at": {
                              "type": "string"
                            },
                            "status": {
                              "type": "object",
                              "properties": {
                                "last_success_at": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "last_error": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "last_error_at": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "exported_count": {
                                  "type": "integer",
                                  "description": "Log records and spans the tool accepted."
                                },
                                "failed_count": {
                                  "type": "integer",
                                  "description": "Export requests that failed (each is retried with backoff)."
                                },
                                "dropped_count": {
                                  "type": "integer",
                                  "description": "Log records and spans given up on after the last retry, or refused (e.g. 401/403)."
                                },
                                "last_test_at": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "last_test_ok": {
                                  "type": [
                                    "boolean",
                                    "null"
                                  ]
                                }
                              }
                            }
                          }
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "vendors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "enum": [
                              "datadog",
                              "grafana_cloud",
                              "otel_collector",
                              "posthog",
                              "sentry"
                            ]
                          },
                          "name": {
                            "type": "string"
                          },
                          "summary": {
                            "type": "string"
                          },
                          "signals": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "enum": [
                                "logs",
                                "traces"
                              ]
                            },
                            "description": "The OTLP signals the vendor ingests. PostHog: logs only."
                          },
                          "docs_url": {
                            "type": "string",
                            "description": "The vendor's own OTLP ingestion documentation."
                          },
                          "fields": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "key": {
                                  "type": "string"
                                },
                                "label": {
                                  "type": "string"
                                },
                                "kind": {
                                  "type": "string",
                                  "enum": [
                                    "text",
                                    "url",
                                    "select",
                                    "secret",
                                    "headers"
                                  ]
                                },
                                "secret": {
                                  "type": "boolean",
                                  "description": "Stored AES-256-GCM encrypted; never returned."
                                },
                                "required": {
                                  "type": "boolean"
                                },
                                "placeholder": {
                                  "type": "string"
                                },
                                "help": {
                                  "type": "string"
                                },
                                "options": {
                                  "type": "array",
                                  "items": {
                                    "type": "object",
                                    "properties": {
                                      "value": {
                                        "type": "string"
                                      },
                                      "label": {
                                        "type": "string"
                                      }
                                    }
                                  }
                                },
                                "default": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Project not found in this workspace."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createBroadcastLogs",
        "summary": "Start forwarding the project's logs and traces over OTLP/HTTP (JSON) to one tool",
        "description": "A project has ONE integration. When one exists the request is refused with 409 unless `replace` is exactly `true`; a replace deletes the old integration (settings, credentials and counts) and is audited as replace_telemetry_integration. Endpoint URLs must be https, public hosts (no localhost, private, link-local or metadata addresses) and carry no credentials. Audited as create_telemetry_integration.",
        "tags": [
          "Broadcast Logs"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "type": "object",
                    "properties": {
                      "settings": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "description": "Non-secret fields of the chosen vendor (see `vendors[].fields`)."
                      },
                      "secrets": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "description": "Secret fields. On PATCH and on a test of the same vendor, a blank or missing secret keeps the stored value."
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "vendor"
                    ],
                    "properties": {
                      "vendor": {
                        "type": "string",
                        "enum": [
                          "datadog",
                          "grafana_cloud",
                          "otel_collector",
                          "posthog",
                          "sentry"
                        ]
                      },
                      "enabled": {
                        "type": "boolean",
                        "default": true
                      },
                      "replace": {
                        "type": "boolean",
                        "description": "Must be true to replace an existing integration."
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created (or replaced).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "integration": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "vendor": {
                          "type": "string",
                          "enum": [
                            "datadog",
                            "grafana_cloud",
                            "otel_collector",
                            "posthog",
                            "sentry"
                          ]
                        },
                        "vendor_name": {
                          "type": "string"
                        },
                        "signals": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "logs",
                              "traces"
                            ]
                          }
                        },
                        "settings": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          },
                          "description": "The vendor's non-secret settings (site, region, endpoint, instance id)."
                        },
                        "secrets_set": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "The NAMES of the secret fields that are set. Secret values are never returned."
                        },
                        "secrets_unreadable": {
                          "type": "boolean",
                          "description": "True when the stored credentials cannot be decrypted; nothing is sent until they are saved again."
                        },
                        "endpoints": {
                          "type": "object",
                          "properties": {
                            "logs": {
                              "type": "string"
                            },
                            "traces": {
                              "type": "string"
                            }
                          },
                          "description": "Where each signal is POSTed (URLs only; credentials travel in headers)."
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "created_by": {
                          "type": "string"
                        },
                        "created_at": {
                          "type": "string"
                        },
                        "updated_at": {
                          "type": "string"
                        },
                        "status": {
                          "type": "object",
                          "properties": {
                            "last_success_at": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "last_error": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "last_error_at": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "exported_count": {
                              "type": "integer",
                              "description": "Log records and spans the tool accepted."
                            },
                            "failed_count": {
                              "type": "integer",
                              "description": "Export requests that failed (each is retried with backoff)."
                            },
                            "dropped_count": {
                              "type": "integer",
                              "description": "Log records and spans given up on after the last retry, or refused (e.g. 401/403)."
                            },
                            "last_test_at": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "last_test_ok": {
                              "type": [
                                "boolean",
                                "null"
                              ]
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing or invalid, or an endpoint is not an allowed outbound URL."
          },
          "403": {
            "description": "Owner or admin only. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The project already has an integration and `replace: true` was not sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "integration_exists"
                      ]
                    },
                    "existing": {
                      "type": "object",
                      "properties": {
                        "vendor": {
                          "type": "string"
                        },
                        "vendor_name": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateBroadcastLogs",
        "summary": "Change the integration's settings or secrets, or enable / disable it",
        "description": "The vendor cannot change here (that is a replace). Secrets left blank keep their stored value. Disabling stops forwarding at once, including anything already queued. Audited as update_telemetry_integration with the names (never the values) of changed secrets.",
        "tags": [
          "Broadcast Logs"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "type": "object",
                    "properties": {
                      "settings": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "description": "Non-secret fields of the chosen vendor (see `vendors[].fields`)."
                      },
                      "secrets": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "description": "Secret fields. On PATCH and on a test of the same vendor, a blank or missing secret keeps the stored value."
                      }
                    }
                  },
                  {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean"
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "integration": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "vendor": {
                          "type": "string",
                          "enum": [
                            "datadog",
                            "grafana_cloud",
                            "otel_collector",
                            "posthog",
                            "sentry"
                          ]
                        },
                        "vendor_name": {
                          "type": "string"
                        },
                        "signals": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "logs",
                              "traces"
                            ]
                          }
                        },
                        "settings": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          },
                          "description": "The vendor's non-secret settings (site, region, endpoint, instance id)."
                        },
                        "secrets_set": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "The NAMES of the secret fields that are set. Secret values are never returned."
                        },
                        "secrets_unreadable": {
                          "type": "boolean",
                          "description": "True when the stored credentials cannot be decrypted; nothing is sent until they are saved again."
                        },
                        "endpoints": {
                          "type": "object",
                          "properties": {
                            "logs": {
                              "type": "string"
                            },
                            "traces": {
                              "type": "string"
                            }
                          },
                          "description": "Where each signal is POSTed (URLs only; credentials travel in headers)."
                        },
                        "enabled": {
                          "type": "boolean"
                        },
                        "created_by": {
                          "type": "string"
                        },
                        "created_at": {
                          "type": "string"
                        },
                        "updated_at": {
                          "type": "string"
                        },
                        "status": {
                          "type": "object",
                          "properties": {
                            "last_success_at": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "last_error": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "last_error_at": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "exported_count": {
                              "type": "integer",
                              "description": "Log records and spans the tool accepted."
                            },
                            "failed_count": {
                              "type": "integer",
                              "description": "Export requests that failed (each is retried with backoff)."
                            },
                            "dropped_count": {
                              "type": "integer",
                              "description": "Log records and spans given up on after the last retry, or refused (e.g. 401/403)."
                            },
                            "last_test_at": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "last_test_ok": {
                              "type": [
                                "boolean",
                                "null"
                              ]
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid field, nothing to change, or an attempt to change the vendor."
          },
          "403": {
            "description": "Owner or admin only. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The project has no integration."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "deleteBroadcastLogs",
        "summary": "Remove the integration",
        "description": "Deletes its settings, credentials and counts. Audited as delete_telemetry_integration.",
        "tags": [
          "Broadcast Logs"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Owner or admin only. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The project has no integration."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/telemetry/test": {
      "post": {
        "operationId": "testBroadcastLogs",
        "summary": "Send one test log record (and span) to a draft or the saved configuration",
        "description": "With `vendor` (plus `settings` / `secrets`) in the body, tests that draft without saving it; a draft of the saved vendor may leave secrets blank to use the stored ones. With an empty body, tests the saved integration and records `last_test_at` / `last_test_ok`. Redirects are never followed.",
        "tags": [
          "Broadcast Logs"
        ],
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "type": "object",
                    "properties": {
                      "settings": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "description": "Non-secret fields of the chosen vendor (see `vendors[].fields`)."
                      },
                      "secrets": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "string"
                        },
                        "description": "Secret fields. On PATCH and on a test of the same vendor, a blank or missing secret keeps the stored value."
                      }
                    }
                  },
                  {
                    "type": "object",
                    "properties": {
                      "vendor": {
                        "type": "string",
                        "enum": [
                          "datadog",
                          "grafana_cloud",
                          "otel_collector",
                          "posthog",
                          "sentry"
                        ]
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What each signal's endpoint answered.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "Every signal accepted the test event."
                    },
                    "vendor": {
                      "type": "string",
                      "enum": [
                        "datadog",
                        "grafana_cloud",
                        "otel_collector",
                        "posthog",
                        "sentry"
                      ]
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "signal": {
                            "type": "string",
                            "enum": [
                              "logs",
                              "traces"
                            ]
                          },
                          "url": {
                            "type": "string"
                          },
                          "ok": {
                            "type": "boolean"
                          },
                          "status": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "ms": {
                            "type": "integer"
                          },
                          "error": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid draft."
          },
          "403": {
            "description": "Owner or admin only. Also `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No saved integration to test."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/portal/api/session": {
      "post": {
        "operationId": "startPortalSession",
        "summary": "Trade a portal token for a session cookie",
        "description": "What the portal page does on first load: the link carries the token in its fragment (`#token=hpt_\u2026`, never sent to a server), the page presents it here as a bearer, receives the `__Host-hookie_portal` session cookie, and forgets the token. The session lasts 8 hours or until the token expires, whichever is sooner. A session cannot be used to start another one.",
        "tags": [
          "Customer Portal"
        ],
        "security": [
          {
            "portalTokenHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Session started. The cookie is in `Set-Cookie`; the body says when it ends.",
            "headers": {
              "Set-Cookie": {
                "schema": {
                  "type": "string"
                },
                "description": "`__Host-hookie_portal=\u2026; Path=/; HttpOnly; Secure; SameSite=None; Partitioned; Max-Age=\u2026`"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "expires_at"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "ok": true,
                  "expires_at": "2026-09-29T20:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Called with the session cookie instead of a bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired token, or the portal is switched off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 calls a minute per token).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "endPortalSession",
        "summary": "Sign out of the portal",
        "description": "Clears the session cookie. Needs `X-Requested-With: fetch`, and no valid session. The token itself stays valid until it is revoked or expires.",
        "tags": [
          "Customer Portal"
        ],
        "security": [
          {
            "portalSession": []
          },
          {}
        ],
        "responses": {
          "200": {
            "description": "The cookie is cleared (`Max-Age=0`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`Missing X-Requested-With header`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/webhooks/{id}/rotate": {
      "post": {
        "operationId": "rotateWebhookUrl",
        "summary": "Give an endpoint a new public URL, with an overlap",
        "tags": [
          "Webhooks"
        ],
        "description": "Top-level alias: POST /admin/api/webhooks/{id}/rotate. Write role required. Mints a new high-entropy slug for the SAME endpoint: its id, dataset, settings and history are unchanged. The old URL keeps being accepted for overlap_hours (default 24), then answers 404. Rotating again inside an overlap ends the older URL at once - one previous URL is kept at a time. Audited as rotate_webhook_url (overlap and deadline only, never either slug).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "overlap_hours": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 720,
                    "default": 24,
                    "description": "How long the OLD credential keeps working, in hours (at most 30 days). 0 ends it at once."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rotated. public_url is the new URL; previous_public_url and previous_url_expires_at are null when overlap_hours was 0.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "webhook_slug",
                    "public_path",
                    "public_url",
                    "previous_public_url",
                    "previous_url_expires_at"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "The same webhook id as before."
                    },
                    "webhook_slug": {
                      "type": "string",
                      "description": "The new credential. Never log it."
                    },
                    "public_path": {
                      "type": "string"
                    },
                    "public_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "previous_public_url": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uri"
                    },
                    "previous_url_expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "overlap_hours is not a number from 0 to 720.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Endpoint not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The URL changed while it was being rotated (a concurrent rotation). Load it again and retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/webhooks/{id}/rotate-verification-secret": {
      "post": {
        "operationId": "rotateWebhookVerificationSecret",
        "summary": "Replace an endpoint's provider signing secret, with an overlap",
        "tags": [
          "Webhooks"
        ],
        "description": "Top-level alias: POST /admin/api/webhooks/{id}/rotate-verification-secret. Write role required. For when the provider rolls its signing secret (#210). `secret` is the provider's NEW secret; the one it replaces keeps verifying until overlap_hours from now (default 24; 0 retires it at once), so events signed either way are accepted while the provider switches. Rotating again inside an overlap ends the older secret at once. Neither secret is returned or audited; audited as rotate_webhook_verification_secret with the scheme, overlap and deadline.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "secret"
                ],
                "additionalProperties": false,
                "properties": {
                  "secret": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "writeOnly": true,
                    "description": "The provider's new signing secret. Never returned."
                  },
                  "overlap_hours": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 720,
                    "default": 24,
                    "description": "How long the OLD secret keeps verifying, in hours (at most 30 days). 0 ends it at once."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rotated. verification.previous_secret_expires_at is the old secret's deadline, or null when overlap_hours was 0.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "verification"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "verification": {
                      "$ref": "#/components/schemas/EndpointVerification"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "secret missing or invalid for the scheme, or overlap_hours not a number from 0 to 720.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Endpoint not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The endpoint does not verify signatures (scheme none); set verification with PATCH first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/ingest-keys/{id}/rotate": {
      "post": {
        "operationId": "rotateIngestKey",
        "summary": "Rotate an ingest key with an overlap",
        "tags": [
          "Ingest keys"
        ],
        "description": "Top-level alias: POST /admin/api/ingest-keys/{id}/rotate. Write role required. Issues a NEW key with the old one's name, dataset_default, require_signature and ip_allowlist, and gives the OLD key an expiry overlap_hours from now (an earlier expiry it already had is kept). Both authenticate until then, at ingest and on /v1/stream. A key that requires signatures gets a new signing secret. The new key and secret are returned once, exactly as on create. A key can be rotated once; rotate its replacement after that. Audited as rotate_ingest_key on the old key, naming the new one.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Ingest key id (the key being replaced).",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "overlap_hours": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 720,
                    "default": 24,
                    "description": "How long the OLD credential keeps working, in hours (at most 30 days). 0 ends it at once."
                  },
                  "expires_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "Optional expiry for the NEW key. Default: none."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new key. key and signing_secret are shown exactly once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "key",
                    "key_prefix",
                    "signing_secret",
                    "expires_at",
                    "previous_key"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string",
                      "description": "ik_live_ followed by 48 hex characters."
                    },
                    "key_prefix": {
                      "type": "string"
                    },
                    "signing_secret": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "A new whsec_ secret when the key requires signatures; otherwise null."
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "previous_key": {
                      "type": "object",
                      "required": [
                        "id",
                        "expires_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the old key stops working."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "overlap_hours is not a number from 0 to 720, or expires_at is not a future timestamp.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required: owner, admin or developer), or a cookie-authenticated mutation missing the X-Requested-With: fetch header. Also `reason: \"workspace_suspended\"` while the workspace is suspended. For an OAuth-connected agent the body also carries `code`, `required_scope` and `granted_scopes`, plus `manage_url` when widening its grant in Settings \u2192 Connected agents would let the call through (see Error).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Key not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The key is revoked, expired, or already rotated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/api-keys": {
      "get": {
        "operationId": "listApiKeys",
        "summary": "List the workspace's admin API keys",
        "tags": [
          "Workspace"
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Every key in the workspace, newest first, revoked and expired ones included. Never the key or its hash.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "keys"
                  ],
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "role",
                          "project_id",
                          "project_name",
                          "key_prefix",
                          "ip_allowlist",
                          "expires_at",
                          "created_by",
                          "created_by_email",
                          "created_at",
                          "last_used_at",
                          "revoked_at",
                          "status"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "viewer",
                              "developer",
                              "admin"
                            ]
                          },
                          "project_id": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Null: every project."
                          },
                          "project_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "key_prefix": {
                            "type": "string",
                            "description": "The first 11 characters, for recognising a key. Never used to authenticate."
                          },
                          "ip_allowlist": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "As stored: a JSON array of CIDRs, or null for any address."
                          },
                          "expires_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_by": {
                            "type": "string",
                            "description": "The member the key acts as."
                          },
                          "created_by_email": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "last_used_at": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Refreshed at most once a minute."
                          },
                          "revoked_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "expired",
                              "revoked"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or an invalid, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` \u2014 owner or admin (or an admin-role credential) only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createApiKey",
        "summary": "Create an admin API key (shown once)",
        "description": "A person in the console only: a connected agent or another API key is refused, so one credential cannot mint a lasting foothold. The key never has more access than its creator, is re-clipped to the creator's current role on every request, and stops working if they leave. Does not accept `Idempotency-Key` (400): the response is a secret that is never stored.",
        "tags": [
          "Workspace"
        ],
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "role"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "developer",
                      "admin"
                    ]
                  },
                  "project_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Limit the key to one project's `projects/{id}/\u2026` routes. Omit or null for every project."
                  },
                  "expires_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "ISO 8601, in the future. Omit or null for no expiry."
                  },
                  "ip_allowlist": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 64,
                    "description": "IPs or CIDRs (v4 or v6). Empty or null: any address. Other addresses get 403."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. `key` is in this response and nowhere else, ever.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "key",
                    "key_prefix",
                    "name",
                    "role",
                    "project_id",
                    "expires_at",
                    "ip_allowlist",
                    "created_at"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "key": {
                      "type": "string",
                      "example": "hk_3f9a1c2e\u2026",
                      "description": "`hk_` followed by 48 hex characters."
                    },
                    "key_prefix": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "viewer",
                        "developer",
                        "admin"
                      ]
                    },
                    "project_id": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "expires_at": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "ip_allowlist": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "created_at": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or over-long name, an unknown role, a bad expiry or allowlist, or an `Idempotency-Key` header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, a role above the creator's own, or the caller is an agent or API key (`code: \"agent_not_permitted\"`; `claim_required: true` for an unclaimed self-registered agent account).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Project not found` \u2014 `project_id` is not a project in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/api-keys/{key_id}": {
      "delete": {
        "operationId": "revokeApiKey",
        "summary": "Revoke an admin API key",
        "description": "Effective on the key's next request. Allowed while the workspace is suspended.",
        "tags": [
          "Workspace"
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`API key not found` \u2014 unknown, in another workspace, or already revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/portal/api/destinations/{id}/secret": {
      "get": {
        "operationId": "revealPortalDestinationSecret",
        "summary": "Show a destination's signing secret again",
        "description": "The secret is AES-256-GCM encrypted at rest, so a customer who missed it at creation can read it again (#198) rather than create a second endpoint. Every reveal is audited as reveal_destination_secret.",
        "tags": [
          "Customer Portal"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id, scoped to the token's tenant, project, portal and customer: another customer's id answers 404 like one that never existed.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The current signing secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "signing_secret"
                  ],
                  "properties": {
                    "signing_secret": {
                      "type": "string",
                      "pattern": "^whsec_"
                    }
                  }
                },
                "example": {
                  "signing_secret": "whsec_\u2026"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "no-store"
                  ]
                },
                "description": "The secret is never cached."
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No destination matched.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/portal/api/destinations/{id}/rotate-secret": {
      "post": {
        "operationId": "rotatePortalDestinationSecret",
        "summary": "Rotate a destination's signing secret, with an overlap",
        "description": "The console's rotation (#175, DLV-10): a new secret is minted and the one it replaces keeps signing for 24 hours, so every delivery in that window carries a v1 for each and the receiver can move over without dropping one. Rotating again inside a window replaces the older of the two. Audited as rotate_destination_secret.",
        "tags": [
          "Customer Portal"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id, scoped to the token's tenant, project, portal and customer: another customer's id answers 404 like one that never existed.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The new secret, shown here and revealable later.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "signing_secret",
                    "previous_secret_expires_at"
                  ],
                  "properties": {
                    "signing_secret": {
                      "type": "string",
                      "pattern": "^whsec_"
                    },
                    "previous_secret_expires_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "example": {
                  "signing_secret": "whsec_\u2026",
                  "previous_secret_expires_at": "2026-09-30T10:00:00.000Z"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "no-store"
                  ]
                },
                "description": "The secret is never cached."
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Missing X-Requested-With header` \u2014 a write made with the session cookie must carry `X-Requested-With: fetch`. Not required with a bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No destination matched.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/portal/api/destinations/{id}/expire-previous-secret": {
      "post": {
        "operationId": "expirePortalDestinationPreviousSecret",
        "summary": "End a rotation's overlap early",
        "description": "Once the receiver is on the new secret, only it signs from here on. Audited as expire_destination_previous_secret.",
        "tags": [
          "Customer Portal"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Destination id, scoped to the token's tenant, project, portal and customer: another customer's id answers 404 like one that never existed.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Missing X-Requested-With header` \u2014 a write made with the session cookie must carry `X-Requested-With: fetch`. Not required with a bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No destination matched.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/portal/api/deliveries/{id}": {
      "get": {
        "operationId": "getPortalDelivery",
        "summary": "One delivery, whole: why it failed",
        "tags": [
          "Customer Portal"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Delivery id, scoped to this customer's destinations in the portal's project.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The delivery with the endpoint it went to and the full stored response (up to 16 KiB).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "delivery"
                  ],
                  "properties": {
                    "delivery": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "destination_id": {
                          "type": "string"
                        },
                        "destination_name": {
                          "type": "string"
                        },
                        "destination_url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "dataset": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "response_code": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "response_ms": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "attempts": {
                          "type": "integer"
                        },
                        "max_attempts": {
                          "type": "integer"
                        },
                        "next_retry_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time"
                        },
                        "error": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "response_body": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "response_content_type": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No delivery matched for this customer. Body: {\"error\":\"Delivery not found\"}.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/portal/api/deliveries/{id}/replay": {
      "post": {
        "operationId": "replayPortalDelivery",
        "summary": "Send a delivery again",
        "description": "The console's single replay (#198, same code): a new delivery of the same event to the same destination, charged to the vendor's monthly delivery allowance (#159) and refused, not held, when none is left. Audited as replay_delivery with the customer as the actor.",
        "tags": [
          "Customer Portal"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Delivery id, scoped to this customer's destinations in the portal's project.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Queued.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "The new delivery's id."
                    }
                  }
                },
                "example": {
                  "id": "9a0c\u2026"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Missing X-Requested-With header` \u2014 a write made with the session cookie must carry `X-Requested-With: fetch`. Not required with a bearer token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No delivery matched for this customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It cannot be sent: the delivery is still queued or being sent, the destination was deleted or is switched off, the portal no longer shares that dataset, or the event is past the plan's retention.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Either the per-token rate limit, or the vendor's delivery allowance is spent (`reason: \"delivery_allowance_exhausted\"`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/portal/api/examples": {
      "get": {
        "operationId": "getPortalExamples",
        "summary": "Example payloads for each exposed event type, and how to verify a delivery (both signature schemes)",
        "description": "For each dataset the portal exposes (#198, PORT-11): the JSON envelope Hookie POSTs, around the shape of the dataset's latest record with every value replaced (strings `\"<string>\"`, numbers 0, booleans false, arrays one element, at most 6 levels and 50 keys). A portal's customers share its datasets, so no value from a record is ever returned \u2014 only field names. A dataset with no record yet shows the envelope with empty `data`. The response documents both signatures every delivery carries over one timestamp: `signature` (Hookie-Signature) and, since #245, `standard_webhooks` (the Standard Webhooks `webhook-id` / `webhook-timestamp` / `webhook-signature` set added by #192, DLV-12), which a receiver can check with an official `standardwebhooks` library by passing the `whsec_` signing secret as is.",
        "tags": [
          "Customer Portal"
        ],
        "responses": {
          "200": {
            "description": "Examples and the signature scheme.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "examples",
                    "signature",
                    "standard_webhooks",
                    "headers"
                  ],
                  "properties": {
                    "examples": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "dataset",
                          "source",
                          "payload"
                        ],
                        "properties": {
                          "dataset": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "recent_record_redacted",
                              "none"
                            ]
                          },
                          "payload": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "dataset": {
                                "type": "string"
                              },
                              "received_at": {
                                "type": "string"
                              },
                              "data": {
                                "type": "object"
                              }
                            }
                          }
                        }
                      }
                    },
                    "signature": {
                      "type": "object",
                      "properties": {
                        "header": {
                          "type": "string",
                          "enum": [
                            "Hookie-Signature"
                          ]
                        },
                        "format": {
                          "type": "string"
                        },
                        "signed_content": {
                          "type": "string"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    },
                    "standard_webhooks": {
                      "type": "object",
                      "description": "The Standard Webhooks signature (https://www.standardwebhooks.com) every delivery also carries (DLV-12). `webhook-id` is the delivery id, so a retry repeats it; during a secret rotation `webhook-signature` carries one space-separated `v1,` value per live secret.",
                      "required": [
                        "headers",
                        "format",
                        "signed_content",
                        "secret",
                        "libraries",
                        "spec",
                        "note"
                      ],
                      "properties": {
                        "headers": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "webhook-id",
                              "webhook-timestamp",
                              "webhook-signature"
                            ]
                          }
                        },
                        "format": {
                          "type": "string"
                        },
                        "signed_content": {
                          "type": "string"
                        },
                        "secret": {
                          "type": "string",
                          "description": "How to hand the signing secret to a library: the `whsec_` value exactly as shown, not stripped or decoded."
                        },
                        "libraries": {
                          "type": "string",
                          "format": "uri",
                          "description": "The official Standard Webhooks libraries."
                        },
                        "spec": {
                          "type": "string",
                          "format": "uri"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    },
                    "headers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Every header a delivery carries: Hookie's own and the Standard Webhooks set."
                    }
                  }
                },
                "example": {
                  "examples": [
                    {
                      "dataset": "orders",
                      "source": "recent_record_redacted",
                      "payload": {
                        "id": "<event id>",
                        "dataset": "orders",
                        "received_at": "<ISO 8601 timestamp>",
                        "data": {
                          "order_id": "<string>",
                          "amount": 0
                        }
                      }
                    }
                  ],
                  "signature": {
                    "header": "Hookie-Signature",
                    "format": "t=<unix seconds>,v1=<hex HMAC-SHA256>",
                    "signed_content": "<t>.<raw request body>",
                    "note": "During a secret rotation the header carries one v1 per live secret for 24 hours; accept the request when any v1 matches."
                  },
                  "standard_webhooks": {
                    "headers": [
                      "webhook-id",
                      "webhook-timestamp",
                      "webhook-signature"
                    ],
                    "format": "v1,<base64 HMAC-SHA256>",
                    "signed_content": "<webhook-id>.<webhook-timestamp>.<raw request body>",
                    "secret": "Pass your whsec_ signing secret to the library exactly as shown. Do not strip the prefix or decode it: the library does both.",
                    "libraries": "https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries",
                    "spec": "https://www.standardwebhooks.com",
                    "note": "webhook-id is the delivery id, so a retry repeats it. During a secret rotation webhook-signature carries one space-separated v1 per live secret for 24 hours; a library accepts the request when any matches."
                  },
                  "headers": [
                    "Hookie-Signature",
                    "Hookie-Event-Id",
                    "Hookie-Delivery-Id",
                    "Idempotency-Key",
                    "webhook-id",
                    "webhook-timestamp",
                    "webhook-signature"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked, expired token, or the portal is disabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: each portal token may make 60 calls a minute across the portal API. Retry after the `Retry-After` header (60 seconds).",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "portalTokenHeader": []
          },
          {
            "portalSession": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/rules/test": {
      "post": {
        "operationId": "testRule",
        "summary": "Test conditions and mappings against a sample payload",
        "tags": [
          "Rules"
        ],
        "description": "Whether these conditions match the payload and what the mappings would store, evaluated by the same engine ingest uses. Validated exactly as a save is; nothing is written. Readable by any role. The console's rule editor calls it (ING-12, #201).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "payload"
                ],
                "properties": {
                  "conditions": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Condition"
                    },
                    "maxItems": 10
                  },
                  "mappings": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "payload": {
                    "description": "The sample event."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The verdict.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "matched",
                    "record"
                  ],
                  "properties": {
                    "matched": {
                      "type": "boolean"
                    },
                    "record": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "description": "What the rule would store; null when it does not match."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "payload missing, or invalid conditions or mappings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/config": {
      "get": {
        "operationId": "exportProjectConfig",
        "summary": "Export a project's configuration",
        "tags": [
          "Projects"
        ],
        "description": "The project's configuration as JSON, or as YAML with ?format=yaml (the subset `hookie apply` reads). Readable by any role (DX-10, #201).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "yaml"
              ],
              "default": "json"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The document.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectConfig"
                }
              },
              "application/yaml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "importProjectConfig",
        "summary": "Import a configuration into a project",
        "tags": [
          "Projects"
        ],
        "description": "Create what the document describes, in dependency order (AI agents, record views, rules, endpoints, destinations, workflows, AI triggers, cron triggers), each through its own create route \u2014 so it is validated, plan-limited and audited as a create by hand is, and every endpoint gets a NEW URL and every destination a NEW signing secret (reveal it with GET destinations/{id}/secret). An endpoint that verifies a provider's signature is created with its scheme and settings but NO secret, so it refuses every request with 401 until one is set, and is listed under needs_verification_secret (#238). A resource whose identity already exists in the project is skipped, never overwritten. A failed create is reported and the import carries on. JSON only; import YAML with `hookie apply`. Write role required; cron triggers need owner or admin.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectConfig"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was created, skipped and failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfigImportResult"
                }
              }
            }
          },
          "400": {
            "description": "Not a configuration document: not an object, an unknown top-level key, an unsupported version, a kind that is not a list, or more than 250 resources in all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (write capability required), or a cookie-authenticated request missing X-Requested-With: fetch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/clone": {
      "post": {
        "operationId": "cloneProject",
        "summary": "Clone a project",
        "tags": [
          "Projects"
        ],
        "description": "A new project, of the same type, holding this project's configuration: an export imported into it (see importProjectConfig). No data is copied, no secret either \u2014 endpoints get new URLs, destinations new signing secrets, an endpoint that verifies a provider's signature keeps its scheme and settings but not its secret (so it refuses every request with 401 until one is set; listed under import.needs_verification_secret), and the new project gets no seeded endpoint. Owner or admin (DX-10, #201).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new project and what its import did.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "project",
                    "import"
                  ],
                  "properties": {
                    "project": {
                      "type": "object"
                    },
                    "import": {
                      "$ref": "#/components/schemas/ConfigImportResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "name missing or longer than 80 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role (owner or admin).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/webhooks/{id}/activity": {
      "get": {
        "operationId": "getWebhookActivity",
        "summary": "One endpoint's received and refused requests",
        "tags": [
          "Webhooks"
        ],
        "description": "OBS-3 (#193). Top-level alias: GET /admin/api/webhooks/{id}/activity. Any role. Newest first over the last 7 days: the endpoint's submissions (kind received) and its rows in the rejection log (kind rejected). The rejection log records every request refused after the endpoint (or ingest key) resolved - IP allowlist, signature, monthly quota, burst rate limit, disabled endpoint, 413, 415, unparseable or empty body, loop, failed URL verification, disallowed CORS origin, suspended workspace - with its reason, status, source IP, method and content type. A request to an unknown URL or key is not logged: there is no workspace to log it against. One row per credential, reason and UTC minute, with repeats counted; pruned on the plan's retention window and deleted with the project. Refusals never count against the event quota. `summary` covers the last 24 hours.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "received",
                "rejected"
              ],
              "default": "all"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the endpoint's activity.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "endpoint_id",
                    "summary",
                    "rows",
                    "total",
                    "limit",
                    "offset",
                    "since"
                  ],
                  "properties": {
                    "endpoint_id": {
                      "type": "string"
                    },
                    "summary": {
                      "type": "object",
                      "required": [
                        "window_hours",
                        "received",
                        "rejected",
                        "filtered",
                        "last_received_at",
                        "last_rejected_at"
                      ],
                      "properties": {
                        "window_hours": {
                          "type": "integer",
                          "const": 24
                        },
                        "received": {
                          "type": "integer",
                          "description": "Submissions in the last 24 hours."
                        },
                        "rejected": {
                          "type": "integer",
                          "description": "Refused requests in the last 24 hours (the sum of counts)."
                        },
                        "filtered": {
                          "type": "integer",
                          "description": "Of the submissions received in the last 24 hours, how many the endpoint's own criteria rejected (forward_status 'filtered', #287). They are kept and counted in `received`; the project's Errors tab lists them."
                        },
                        "last_received_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "Within the last 7 days."
                        },
                        "last_rejected_at": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "format": "date-time",
                          "description": "Within the last 7 days."
                        }
                      }
                    },
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "kind",
                          "id",
                          "at",
                          "reason",
                          "status",
                          "count",
                          "source_ip",
                          "method",
                          "content_type",
                          "forward_status"
                        ],
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "received",
                              "rejected"
                            ]
                          },
                          "id": {
                            "type": "string",
                            "description": "The submission id (received), or the rejection-log row id (rejected)."
                          },
                          "at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When it was received; for a rejected row, the last refusal in its minute."
                          },
                          "reason": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "enum": [
                              "endpoint_disabled",
                              "dataset_not_allowed",
                              "ip_not_allowed",
                              "workspace_suspended",
                              "rate_limited",
                              "body_too_large",
                              "handshake_failed",
                              "invalid_body",
                              "unsupported_media_type",
                              "empty_body",
                              "signature_required",
                              "signature_invalid",
                              "signature_stale",
                              "signature_misconfigured",
                              "unknown_endpoint",
                              "loop_detected",
                              "quota_exceeded",
                              "cors_origin_not_allowed",
                              null
                            ],
                            "description": "Rejected only."
                          },
                          "status": {
                            "type": [
                              "integer",
                              "null"
                            ],
                            "description": "Rejected only: the HTTP status the sender got."
                          },
                          "count": {
                            "type": "integer",
                            "description": "Rejected rows group one reason per UTC minute: how many requests. 1 for received."
                          },
                          "source_ip": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "For a rejected row, the most recent sender in its minute."
                          },
                          "method": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "content_type": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "forward_status": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Received only."
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "since": {
                      "type": "string",
                      "format": "date-time",
                      "description": "The start of the 7-day window."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "kind is not all, received or rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Endpoint not found in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/datasets/{dataset}/purge": {
      "get": {
        "operationId": "getDatasetPurge",
        "summary": "The dataset's latest purge",
        "tags": [
          "Records"
        ],
        "description": "Progress of the most recent purge of this dataset, or `purge: null` if it was never purged. Readable by any role.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dataset",
            "in": "path",
            "required": true,
            "description": "Dataset name (letters, digits, underscores).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The latest purge.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "purge"
                  ],
                  "properties": {
                    "purge": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/DatasetPurge"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid dataset name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "purgeDataset",
        "summary": "Delete every record in a dataset",
        "tags": [
          "Records"
        ],
        "description": "Deletes every record in the dataset received up to the moment of the request, each with everything DELETE records/{id} takes with it (submission, deliveries, holds, workflow runs, AI output). Records that arrive afterwards are kept. Runs up to 1,000 records inside the request; a larger purge answers 202 and the scheduled sweep (every 5 minutes) finishes it \u2014 poll GET for progress. One running purge per dataset. Owner or admin. Audited as `purge_dataset` (request) and `dataset_purged` (completion, with the count). Not available as an MCP tool.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dataset",
            "in": "path",
            "required": true,
            "description": "Dataset name (letters, digits, underscores).",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirm"
                ],
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "The dataset's name, typed back."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Purged completely (`status: done`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "purge"
                  ],
                  "properties": {
                    "purge": {
                      "$ref": "#/components/schemas/DatasetPurge"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Started; `status: running`, with `records_remaining`. The sweep deletes the rest.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "purge"
                  ],
                  "properties": {
                    "purge": {
                      "$ref": "#/components/schemas/DatasetPurge"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`confirm` does not equal the dataset name, or the name is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Only owners and admins can purge a dataset`, `workspace_suspended`, or `Missing X-Requested-With header`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This dataset is already being purged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/workspace/closure": {
      "get": {
        "operationId": "getWorkspaceClosure",
        "summary": "The pending workspace closure, if any",
        "tags": [
          "Workspace"
        ],
        "description": "Any member. `closure` is null unless the owner has closed the workspace and the grace period is running. The same object is on GET /admin/api/me as `workspaceClosure`.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The pending closure, or null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "closure"
                  ],
                  "properties": {
                    "closure": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/WorkspaceClosure"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "closeWorkspace",
        "summary": "Close the workspace",
        "tags": [
          "Workspace"
        ],
        "description": "Owner only, and never through an OAuth-connected agent. First cancels a live Stripe subscription immediately (if Stripe refuses, nothing changes and the call answers 502); then suspends the workspace (ingest refused, deliveries held, settings read-only \u2014 see `workspace_suspended`), drops it to the Free plan, and schedules the permanent deletion of all of its data 14 days later. Undo with DELETE inside that period. After it, every project, record, submission, delivery, run, setting, key and membership is deleted; the audit log is kept for its own retention (at least a year) and the workspace row remains as a tombstone with its name removed. Audited as `workspace_closure_requested`. Allowed while the workspace is suspended.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirm"
                ],
                "properties": {
                  "confirm": {
                    "type": "string",
                    "description": "The workspace slug (`workspaceSlug` on /me), typed back."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Closing.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "status",
                    "closure",
                    "subscription_cancelled"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "status": {
                      "type": "string",
                      "const": "suspended"
                    },
                    "closure": {
                      "$ref": "#/components/schemas/WorkspaceClosure"
                    },
                    "subscription_cancelled": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`confirm` does not equal the workspace slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not the owner, or the caller is an OAuth-connected agent, or `Missing X-Requested-With header`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The workspace is already closing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "501": {
            "description": "The workspace has a live subscription and billing is not configured, so it cannot be cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Stripe did not cancel the subscription (or could not be reached); the workspace was not closed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      },
      "delete": {
        "operationId": "reopenWorkspace",
        "summary": "Undo a pending closure",
        "tags": [
          "Workspace"
        ],
        "description": "Owner only, not through an agent, inside the grace period. Restores the status the workspace had before it was closed (normally active; an operator's suspension is never lifted this way) and re-queues held deliveries. A subscription cancelled by the closure is not restarted. Audited as `workspace_closure_cancelled`. Allowed while the workspace is suspended.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Reopened.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "status",
                    "closure"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "active",
                        "suspended"
                      ]
                    },
                    "closure": {
                      "type": "null"
                    },
                    "resumed_deliveries": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not the owner, or the caller is an OAuth-connected agent, or `Missing X-Requested-With header`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The workspace is not closing, or the grace period is over and its data is being deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/workspace/export": {
      "get": {
        "operationId": "exportWorkspace",
        "summary": "Export all of the workspace's data",
        "tags": [
          "Workspace"
        ],
        "description": "Owner or admin. One JSON Lines file (`Content-Disposition: attachment`), streamed as it is read so a workspace of any size downloads in constant memory. Lines, in order: `{\"type\":\"header\", \"format\":\"hookie-workspace-export\", \"version\":1, \"workspace\", \"generated_at\", \"tables\", \"omitted_columns\"}`; then per table `{\"type\":\"row\", \"table\", \"row\"}` lines and a `{\"type\":\"table_end\", \"table\", \"rows\"}`; and last `{\"type\":\"end\", \"complete\":true, \"counts\"}`. A file without the `end` line is incomplete; a read that fails writes `{\"type\":\"error\"}` instead. Covers configuration (projects, endpoints, rules, keys, destinations, sources, triggers, workflows, AI agents, portals, Data API, Broadcast Logs, SSO, subscription), members (with email and name), records, submissions, deliveries, holds, AI and workflow runs, AI calls, usage, former workspace slugs, the refused-request log and the audit log. Secrets are never included: signing secrets, destination credentials (`auth_secret`) and secret custom-header values (a secret header is written by name only), endpoint handshake secrets, encrypted source/listener configs and telemetry secrets, key and token hashes, and endpoint URL slugs are omitted, as `omitted_columns` lists. A workflow run's `context` is written redacted the way the run-detail API shows it (#247): credential-named keys, Bearer/Basic values and URL passwords read `[redacted]`. Audited as `export_workspace`.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The export, as JSON Lines.",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Only owners and admins can export the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/projects/{project_id}/errors": {
      "get": {
        "operationId": "listProjectErrors",
        "summary": "A project's flagged events: filtered and routing failed",
        "tags": [
          "Observability"
        ],
        "description": "#287. Any role. The project's Errors tab: submissions an endpoint's own criteria rejected (kind filtered, forward_status 'filtered') and submissions whose records could not be written (kind routing_failed, forward_status 'failed'), newest first. Each is still a submission: kept, counted against the quota when it arrived, searchable in Observability, and pruned by the plan's retention like any other; nothing here is a copy. Payloads rejected before #287 stay forward_status 'pending' and are not listed (no backfill). Paged on the server with limit/offset and a total. MCP: list_project_errors.",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "filtered",
                "routing_failed"
              ],
              "default": "all"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the project's flagged events.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "rows",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProjectError"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows matching `kind`, over every page."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "kind must be all, filtered or routing_failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/projects/{project_id}/errors/{id}": {
      "get": {
        "operationId": "getProjectError",
        "summary": "One flagged event, with its whole payload",
        "tags": [
          "Observability"
        ],
        "description": "#287. Any role. One row of the Errors tab by its submission id, with `payload` whole rather than the list's 500-character preview. 404 for a submission that is not filtered or failed (for instance one routed since).",
        "parameters": [
          {
            "name": "project_id",
            "in": "path",
            "required": true,
            "description": "Project id (UUID) belonging to the caller workspace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Submission id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The flagged event.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "error"
                  ],
                  "properties": {
                    "error": {
                      "$ref": "#/components/schemas/ProjectError"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email domain not allowed for the workspace, or an agent token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not a filtered or failed submission of this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/admin/api/invites": {
      "get": {
        "operationId": "listInvites",
        "summary": "List the workspace's pending invites",
        "tags": [
          "Workspace"
        ],
        "description": "Owner or admin, in the console (#308). Pending only: not yet accepted, revoked or expired. Never a token or its hash: a link is shown once, when it is created.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "Pending invites, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "invites",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "invites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invite"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (owner or admin only), or `agent_not_permitted` \u2014 an OAuth-connected agent or an admin API key, at any scope: who is in a workspace is decided by a person in the console.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createInvite",
        "summary": "Create a shareable invite link",
        "tags": [
          "Workspace"
        ],
        "description": "Owner or admin, in the console (#308). Hookie sends no email: the response carries the link, which is shown this once and never again, and whoever opens it signed in joins the workspace with `role`. The token is 32 random bytes (base64url); only its SHA-256 is stored. The link works once and expires after seven days. Nobody grants above their own role: only the owner can invite an admin. Members per plan, the owner included: Free 1 (the owner only), Pro 3, Team unlimited (billed per seat). At the limit the invite is refused. At most 50 invites may be pending at once. Audited as `member_invited` (never the token). Refused while the workspace is suspended. An `Idempotency-Key` is refused with 400: the response holds a link shown once, which is never stored for replay.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "developer",
                      "viewer"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The invite, the raw token and the link. Store neither: they cannot be shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "invite",
                    "token",
                    "url"
                  ],
                  "properties": {
                    "invite": {
                      "$ref": "#/components/schemas/Invite"
                    },
                    "token": {
                      "type": "string",
                      "pattern": "^[A-Za-z0-9_-]{43}$",
                      "description": "The raw token, shown once."
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "`{APP_ORIGIN}/#/invite/{token}`: the link to share."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`role must be one of viewer, developer, admin`, a body that is not an object, or an `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`; `Only the workspace owner can invite an admin`; `member_limit_reached`; `agent_not_permitted` \u2014 an OAuth-connected agent or an admin API key, at any scope: who is in a workspace is decided by a person in the console. Or `workspace_suspended`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The workspace already has 50 pending invites.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/invites/{invite_id}": {
      "delete": {
        "operationId": "revokeInvite",
        "summary": "Revoke a pending invite",
        "tags": [
          "Workspace"
        ],
        "description": "Owner or admin, in the console (#308). The link stops working at once. Audited as `member_invite_revoked`. Allowed while the workspace is suspended.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "invite_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`, or `agent_not_permitted` \u2014 an OAuth-connected agent or an admin API key, at any scope: who is in a workspace is decided by a person in the console.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Invite not found` \u2014 unknown, in another workspace, or no longer pending (accepted, revoked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/invites/accept": {
      "post": {
        "operationId": "acceptInvite",
        "summary": "Join a workspace with an invite link's token",
        "tags": [
          "Workspace"
        ],
        "description": "Any signed-in person, in the console (#308); never an agent or an API key. The workspace joined is the one the invite belongs to, never one named in the request. This route is resolved WITHOUT the automatic workspace every other admin route creates for someone who has none, so an invitee joins this workspace without first getting an empty one of their own. The link is used once: accepting creates the membership with the invite's role, marks the invite accepted in the same transaction (two accepts racing for one link make one member), makes the workspace the caller's active one, and is audited as `member_invite_accepted`. A person who is already a member gets 200 with `joined: false` and is switched to the workspace; a pending link they open is not spent. A token that is wrong, expired, revoked or already used answers the same 404 with the same words, and so does an invite whose creator could no longer create it (removed, demoted below admin, or, for an admin invite, no longer the owner): an invite is only as good as its creator's authority when it is accepted. The console calls POST /admin/api/invites/preview first and accepts only when the person clicks Join; opening a link never joins by itself. The deployment's email-domain pin applies. On Team, the subscription's seat count follows (best-effort). Members per plan, the owner included: Free 1 (the owner only), Pro 3, Team unlimited (billed per seat).",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The token from the link (`#/invite/{token}`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Joined, or already a member. Either way the workspace is now the caller's active one.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "joined",
                    "tenant_id",
                    "workspace",
                    "role"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "joined": {
                      "type": "boolean",
                      "description": "False: the caller was already a member, and nothing changed but the active workspace."
                    },
                    "tenant_id": {
                      "type": "string"
                    },
                    "workspace": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "slug"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        }
                      }
                    },
                    "role": {
                      "type": "string",
                      "description": "The caller's role in the workspace."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "An `Idempotency-Key` (not supported on invites).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`member_limit_reached` (worded for the invitee: the workspace is full, and its owner or an admin can upgrade it or free a seat); `workspace_suspended` (the inviting workspace is suspended or being closed); `email domain not allowed`; or `agent_not_permitted` \u2014 an OAuth-connected agent or an admin API key, at any scope: who is in a workspace is decided by a person in the console.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The invite link is not valid: wrong, expired, revoked or already used. Deliberately one answer for all four.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/invites/preview": {
      "post": {
        "operationId": "previewInvite",
        "summary": "See what an invite link's token would join, without joining",
        "tags": [
          "Workspace"
        ],
        "description": "Any signed-in person, in the console (#308); never an agent or an API key. Read-only: nothing is joined, spent or written, and like accept it is resolved WITHOUT the automatic workspace every other admin route creates for someone who has none. The console shows this (the workspace, who sent the invite, the role) and joins only when the person clicks Join, so opening a link from anywhere never moves them into a workspace by itself. It is a POST so the token travels in the body, not the URL. Every token that accept would refuse as not found (wrong, expired, revoked, used by someone else, or whose creator could no longer create it) answers the same 404 with the same words. Whether the workspace is full or suspended is decided when joining.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The token from the link (`#/invite/{token}`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The invite, as the person would join it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspace",
                    "role",
                    "invited_by_email",
                    "expires_at",
                    "already_member"
                  ],
                  "properties": {
                    "workspace": {
                      "type": "object",
                      "required": [
                        "name",
                        "slug"
                      ],
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string",
                          "description": "The workspace's address, to tell apart two workspaces with the same name."
                        }
                      }
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "admin",
                        "developer",
                        "viewer",
                        "owner",
                        "member"
                      ],
                      "description": "The invite's role, or the caller's own role when they are already a member."
                    },
                    "invited_by_email": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The email of the member who created the invite."
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "already_member": {
                      "type": "boolean",
                      "description": "True: accepting would only switch the caller to the workspace."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "An `Idempotency-Key` (not supported on invites).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`email domain not allowed`, or `agent_not_permitted` \u2014 an OAuth-connected agent or an admin API key, at any scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The invite link is not valid: wrong, expired, revoked or already used. Deliberately one answer for all four.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/members": {
      "get": {
        "operationId": "listMembers",
        "summary": "List the workspace's members",
        "tags": [
          "Workspace"
        ],
        "description": "Any role, and an agent or admin API key at `hookie:read` (MCP: `list_members`) (#308). Owner first, then by when each joined. Members per plan, the owner included: Free 1 (the owner only), Pro 3, Team unlimited (billed per seat).",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "The members.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "members",
                    "total",
                    "limit",
                    "offset",
                    "max_members",
                    "plan"
                  ],
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Member"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "max_members": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "The plan's member limit, the owner included. Null: unlimited (Team)."
                    },
                    "plan": {
                      "type": "string",
                      "enum": [
                        "free",
                        "standard",
                        "team"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or an invalid, revoked or expired token or key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "A project-scoped API key (members are workspace-wide), or a token granted no Hookie scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/members/{user_id}": {
      "patch": {
        "operationId": "updateMemberRole",
        "summary": "Change a member's role",
        "tags": [
          "Workspace"
        ],
        "description": "Owner or admin, in the console (#308). The owner's role is never changed here, and `owner` is never set: ownership moves with POST /admin/api/members/transfer-ownership. Nobody grants above their own role: only the owner can make someone an admin. Nobody changes their own role. Demoting an admin below admin revokes the pending invite links they created. Audited as `member_role_changed` with from and to. Refused while the workspace is suspended.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "developer",
                      "viewer"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved (or unchanged). The member as they now are.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "member"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "member": {
                      "$ref": "#/components/schemas/Member"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`role must be one of viewer, developer, admin`, or `owner` (use transfer).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role`; the target is the owner; your own role; an admin making an admin; `agent_not_permitted` \u2014 an OAuth-connected agent or an admin API key, at any scope: who is in a workspace is decided by a person in the console. Or `workspace_suspended`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Member not found` \u2014 not a member of this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The member's role changed meanwhile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "removeMember",
        "summary": "Remove a member, or leave the workspace",
        "tags": [
          "Workspace"
        ],
        "description": "An owner or admin removes a member; any member removes themselves (leaves) (#308). The owner can neither leave nor be removed: they transfer ownership first. The removed member's admin API keys for this workspace stop working at once (a key acts as its creator, who must still be a member), the pending invite links they created are revoked (so a link they kept cannot bring them, or anyone else, back in), and if this was their active workspace their next sign-in lands in another. Audited as `member_removed` or `member_left`. On Team, the subscription's seat count follows (best-effort). Allowed while the workspace is suspended.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "user_id",
                    "left"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "user_id": {
                      "type": "string"
                    },
                    "left": {
                      "type": "boolean",
                      "description": "True when the caller removed themselves."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (removing someone else); the target is the owner; or `agent_not_permitted` \u2014 an OAuth-connected agent or an admin API key, at any scope: who is in a workspace is decided by a person in the console.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Member not found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/members/transfer-ownership": {
      "post": {
        "operationId": "transferOwnership",
        "summary": "Transfer ownership to another member",
        "tags": [
          "Workspace"
        ],
        "description": "The owner only, in the console (#308). The target must already be a member. In one transaction the target becomes the owner and the previous owner becomes an admin, so the workspace always has exactly one owner. Billing and the workspace slug go with it. An admin cannot invite an admin, so the previous owner's pending admin invite links are revoked; their other links stand. Audited as `ownership_transferred`. Refused while the workspace is suspended.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "user_id"
                ],
                "properties": {
                  "user_id": {
                    "type": "string",
                    "description": "The member who becomes the owner."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transferred.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "owner",
                    "previous_owner"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Member"
                    },
                    "previous_owner": {
                      "$ref": "#/components/schemas/Member"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`user_id is required`, or it is the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`Only the workspace owner can transfer ownership`; `agent_not_permitted` \u2014 an OAuth-connected agent or an admin API key, at any scope: who is in a workspace is decided by a person in the console. Or `workspace_suspended`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Member not found` \u2014 ownership only goes to someone already in the workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The owner changed meanwhile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/workspaces": {
      "get": {
        "operationId": "listWorkspaces",
        "summary": "List the workspaces the caller belongs to",
        "tags": [
          "Workspace"
        ],
        "description": "The console's workspace switcher (#308): every workspace the signed-in person is a member of, with their role there. A person's own list, in the console only: not an agent or an API key.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's workspaces, owned ones first, then by when they joined.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "workspaces",
                    "total",
                    "limit",
                    "offset"
                  ],
                  "properties": {
                    "workspaces": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WorkspaceMembership"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Rows in the whole list, before limit/offset (#201)."
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "offset": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`agent_not_permitted` \u2014 an OAuth-connected agent or an admin API key, at any scope: who is in a workspace is decided by a person in the console.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/workspaces/active": {
      "post": {
        "operationId": "setActiveWorkspace",
        "summary": "Switch the caller's active workspace",
        "tags": [
          "Workspace"
        ],
        "description": "Sets the workspace every following console request is answered in (#308). The caller must be a member of `tenant_id`: it is checked against their own memberships, and a workspace they do not belong to answers the same 404 as one that does not exist. Cookie sessions only. Allowed while the current workspace is suspended.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tenant_id"
                ],
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Switched. The console reloads into it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "workspace"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "workspace": {
                      "$ref": "#/components/schemas/WorkspaceMembership"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`tenant_id is required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`agent_not_permitted` \u2014 an OAuth-connected agent or an admin API key, at any scope: who is in a workspace is decided by a person in the console.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Workspace not found` \u2014 no such workspace, or the caller is not a member.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/workspace/google-domain": {
      "get": {
        "operationId": "getGoogleWorkspaceDomain",
        "summary": "The workspace's Google Workspace domain link",
        "tags": [
          "Workspace"
        ],
        "description": "Any member, an agent or API key included. Google Workspace teams (#326). A workspace can be linked to one Google Workspace domain, and a domain to one workspace. Anyone who then signs in to the console with Google using an account in that domain joins the workspace automatically with the link's `default_role`, before Hookie would create an empty workspace for them, and, when it is their first workspace, it becomes their active one (someone already in other workspaces joins this one as well but is not moved: it appears in their workspace switcher). No WorkOS organization is created: the grouping is Hookie's. Linking is first come, first served among the domain's own Google accounts; a platform operator can unlink a domain or reassign it to another workspace, audited in both workspaces' logs. The domain is Google's `hd` claim, read once at sign-in from Google's OpenID Connect userinfo endpoint with the Google access token WorkOS returns, and sealed into an HttpOnly cookie bound to that session. It is never taken from the email address: a personal Google account on a company address, an email-code sign-in and a GitHub sign-in never join, and gmail.com / googlemail.com can never be linked. A join respects the plan's member limit (at the limit the person is not added, and GET /admin/api/me carries `googleWorkspaceNotice`, which names only their own domain, never the workspace or its headcount), never happens for a suspended or closing workspace, and never re-adds someone who left or was removed, including before this feature shipped (an invite still can). Joins are audited as `member_joined_google_workspace`, and on Team the subscription's seat count follows. Requires the WorkOS environment's Google provider to return OAuth tokens; without that no domain is ever verified, so nothing links and nobody joins.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The link, if any, and what this session could link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "link",
                    "session",
                    "roles"
                  ],
                  "properties": {
                    "link": {
                      "$ref": "#/components/schemas/GoogleWorkspaceLink"
                    },
                    "session": {
                      "type": "object",
                      "required": [
                        "google_workspace_domain",
                        "can_link"
                      ],
                      "properties": {
                        "google_workspace_domain": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The Google Workspace domain Google verified for THIS console session's Google sign-in; null for every other sign-in, a personal Google account, an agent or an API key."
                        },
                        "can_link": {
                          "type": "boolean",
                          "description": "True when the caller is the owner and this session has a verified domain."
                        }
                      }
                    },
                    "roles": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "viewer",
                          "developer"
                        ]
                      },
                      "description": "The roles a link can give."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "put": {
        "operationId": "linkGoogleWorkspaceDomain",
        "summary": "Link the owner's Google Workspace domain",
        "tags": [
          "Workspace"
        ],
        "description": "Owner only, in the console, from a Google sign-in whose Google Workspace domain Google verified; never through an OAuth-connected agent or an API key. The domain linked is ALWAYS the session's, never one chosen by the request: `domain` in the body is only a check, refused with 409 `google_workspace_domain_mismatch` when it differs. Linking another domain replaces this workspace's link (members who joined stay); linking the same one again changes only `default_role` (PATCH changes the role without a Google sign-in). Audited as `google_workspace_linked` (or `google_workspace_updated`). Refused while the workspace is suspended. Google Workspace teams (#326). A workspace can be linked to one Google Workspace domain, and a domain to one workspace. Anyone who then signs in to the console with Google using an account in that domain joins the workspace automatically with the link's `default_role`, before Hookie would create an empty workspace for them, and, when it is their first workspace, it becomes their active one (someone already in other workspaces joins this one as well but is not moved: it appears in their workspace switcher). No WorkOS organization is created: the grouping is Hookie's. Linking is first come, first served among the domain's own Google accounts; a platform operator can unlink a domain or reassign it to another workspace, audited in both workspaces' logs. The domain is Google's `hd` claim, read once at sign-in from Google's OpenID Connect userinfo endpoint with the Google access token WorkOS returns, and sealed into an HttpOnly cookie bound to that session. It is never taken from the email address: a personal Google account on a company address, an email-code sign-in and a GitHub sign-in never join, and gmail.com / googlemail.com can never be linked. A join respects the plan's member limit (at the limit the person is not added, and GET /admin/api/me carries `googleWorkspaceNotice`, which names only their own domain, never the workspace or its headcount), never happens for a suspended or closing workspace, and never re-adds someone who left or was removed, including before this feature shipped (an invite still can). Joins are audited as `member_joined_google_workspace`, and on Team the subscription's seat count follows. Requires the WorkOS environment's Google provider to return OAuth tokens; without that no domain is ever verified, so nothing links and nobody joins.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "default_role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "developer"
                    ],
                    "default": "viewer",
                    "description": "The role people from the domain join with. Never admin or owner."
                  },
                  "domain": {
                    "type": "string",
                    "description": "Optional check: the domain the caller expects to link. It must equal the session's verified domain."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Linked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "link"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "link": {
                      "$ref": "#/components/schemas/GoogleWorkspaceLink"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`default_role` is not viewer or developer, or the body is not a JSON object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not the owner; an OAuth-connected agent or API key (`agent_not_permitted`); this session is not a Google sign-in with a verified Google Workspace domain (`google_workspace_session_required`); or the workspace is suspended (`workspace_suspended`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`domain` differs from the session's verified domain (`google_workspace_domain_mismatch`), or the domain is linked to another workspace (`google_workspace_domain_taken`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "operationId": "updateGoogleWorkspaceDomainRole",
        "summary": "Change the role people from the linked domain join with",
        "tags": [
          "Workspace"
        ],
        "description": "Owner only, in the console; never through an OAuth-connected agent or an API key. Changes only `default_role` on the link this workspace already holds. It claims no domain, so unlike linking it needs no Google sign-in: an owner signed in with an email code or GitHub can change it. Members who already joined keep their roles. Audited as `google_workspace_updated` when the role actually changes. Refused while the workspace is suspended.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "default_role"
                ],
                "properties": {
                  "default_role": {
                    "type": "string",
                    "enum": [
                      "viewer",
                      "developer"
                    ],
                    "description": "The role people from the domain join with. Never admin or owner."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated (or already that role).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "link"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "link": {
                      "$ref": "#/components/schemas/GoogleWorkspaceLink"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`default_role` is missing or not viewer or developer, or the body is not a JSON object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not the owner; an OAuth-connected agent or API key (`agent_not_permitted`); or the workspace is suspended (`workspace_suspended`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "This workspace is not linked to a Google Workspace domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "operationId": "unlinkGoogleWorkspaceDomain",
        "summary": "Unlink the Google Workspace domain",
        "tags": [
          "Workspace"
        ],
        "description": "Owner only, in the console; never through an agent or API key. Stops new joins; everyone who already joined stays a member. Audited as `google_workspace_unlinked`. Allowed while the workspace is suspended, since it only takes access away.",
        "security": [
          {
            "session": []
          },
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Unlinked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "link"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "link": {
                      "type": "null"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not the owner, or an OAuth-connected agent or API key (`agent_not_permitted`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "This workspace is not linked to a Google Workspace domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, or the agent bearer token is invalid, unknown or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/admin/api/sso/requests": {
      "get": {
        "operationId": "getSsoOrgRequest",
        "summary": "Read the workspace's SSO / Directory Sync request",
        "tags": [
          "SSO"
        ],
        "description": "The workspace's latest request for single sign-on or Directory Sync, or null, and whether its plan offers them (`eligible`: Team only). Owner and admin. Never available to connected agents or API keys. While a change is in review, `request` is the change and `approved` the request still in force.",
        "responses": {
          "200": {
            "description": "The latest request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "eligible",
                    "plan",
                    "request",
                    "approved",
                    "portal_intents"
                  ],
                  "properties": {
                    "eligible": {
                      "type": "boolean"
                    },
                    "plan": {
                      "type": "string",
                      "enum": [
                        "free",
                        "standard",
                        "team"
                      ]
                    },
                    "request": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/SsoOrgRequest"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The latest request: the approved one, a change or a first request in review, or the last decided."
                    },
                    "approved": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/SsoOrgRequest"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The approved request in force, if any. Differs from `request` while a change is in review."
                    },
                    "portal_intents": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": [
                          "sso",
                          "dsync"
                        ]
                      },
                      "description": "The Admin Portal intents that can be opened at all in this deployment. Single sign-on (`sso`) is closed until Hookie's sign-in can start an SSO login, because an active SSO connection on a verified domain makes WorkOS refuse every other sign-in method for that domain; today this is `[\"dsync\"]`."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "`Insufficient role` (developer or viewer). `code: \"agent_not_permitted\"`: a connected agent or an admin API key, at every scope (reads included). Requesting single sign-on or Directory Sync can commit Hookie to a recurring WorkOS cost, so it is done by a person in the console. Also refused before the route is reached: `Missing X-Requested-With header` (a session without it), or `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      },
      "post": {
        "operationId": "createSsoOrgRequest",
        "summary": "Request single sign-on or Directory Sync",
        "tags": [
          "SSO"
        ],
        "description": "Files a request for a Hookie operator to review (#325). WorkOS bills every SSO and Directory Sync connection, so nothing is created in WorkOS or in Hookie's organization mapping until an operator approves it; this call only stores the request as `pending`. Owner only, on the Team plan. One request in review (pending) per workspace. Filed while a request is approved, it is a CHANGE request (`supersedes` set): it carries the whole new set of kinds and domains, is reviewed the same way, and on approval updates the same WorkOS organization rather than creating another; the approved request stays in force until then, and a rejected or cancelled change leaves it as it was. Consumer mail domains (gmail.com and the like) are refused.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kinds",
                  "domains"
                ],
                "properties": {
                  "kinds": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "sso",
                        "dsync"
                      ]
                    }
                  },
                  "domains": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10,
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "acme.com"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Anything the operator should know: the identity provider, the number of people."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Stored as pending. Nothing was created in WorkOS.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "request"
                  ],
                  "properties": {
                    "request": {
                      "$ref": "#/components/schemas/SsoOrgRequest"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is invalid: `kinds`, a domain that is not one or is a public mail domain, or a `note` over 1000 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Only the owner may request; `reason: \"plan_upgrade_required\"` below Team. `code: \"agent_not_permitted\"`: a connected agent or an admin API key, at every scope (reads included). Requesting single sign-on or Directory Sync can commit Hookie to a recurring WorkOS cost, so it is done by a person in the console. Also refused before the route is reached: `Missing X-Requested-With header` (a session without it), or `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "reason": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "409": {
            "description": "`reason: \"sso_request_open\"`: the workspace already has a pending or approved request, returned as `request`.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "reason": {
                          "type": "string"
                        },
                        "request": {
                          "$ref": "#/components/schemas/SsoOrgRequest"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/admin/api/sso/requests/{request_id}": {
      "delete": {
        "operationId": "cancelSsoOrgRequest",
        "summary": "Cancel a pending SSO / Directory Sync request",
        "tags": [
          "SSO"
        ],
        "description": "Withdraws a request that is still `pending`. An approved request cannot be cancelled here. Owner only; allowed while the workspace is suspended.",
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "request"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "request": {
                      "$ref": "#/components/schemas/SsoOrgRequest"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Only the owner may cancel. `code: \"agent_not_permitted\"`: a connected agent or an admin API key, at every scope (reads included). Requesting single sign-on or Directory Sync can commit Hookie to a recurring WorkOS cost, so it is done by a person in the console. Also refused before the route is reached: `Missing X-Requested-With header` (a session without it), or `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`Request not found` (in this workspace).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The request is no longer pending.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/admin/api/sso/portal-link": {
      "post": {
        "operationId": "createSsoPortalLink",
        "summary": "Open the WorkOS Admin Portal to set up SSO or Directory Sync",
        "tags": [
          "SSO"
        ],
        "description": "Generates a WorkOS Admin Portal link for the workspace's organization, where the customer's IT admin connects their directory (`intent: \"dsync\"`). Refused unless a Hookie operator has approved the workspace's request, and the request included that intent. `intent: \"sso\"` is refused for every workspace, approved or not (`reason: \"sso_signin_not_available\"`) until Hookie's sign-in can start an SSO login: once an SSO connection is active and a domain verified, WorkOS refuses emailed codes, GitHub and Google for that domain across the whole environment, which would lock every Hookie user at it out. The link expires five minutes after it is made and is never stored, so the console opens it straight away. Owner only, on the Team plan.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "intent"
                ],
                "properties": {
                  "intent": {
                    "type": "string",
                    "enum": [
                      "sso",
                      "dsync"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A single-use, five-minute link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "link",
                    "intent",
                    "expires_in_seconds"
                  ],
                  "properties": {
                    "link": {
                      "type": "string",
                      "format": "uri"
                    },
                    "intent": {
                      "type": "string",
                      "enum": [
                        "sso",
                        "dsync"
                      ]
                    },
                    "expires_in_seconds": {
                      "type": "integer",
                      "example": 300
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`intent must be \"sso\" or \"dsync\"`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`reason: \"sso_signin_not_available\"` (`intent: \"sso\"`, closed for now; no WorkOS call is made), `reason: \"sso_request_not_approved\"` (no approved request yet), `reason: \"sso_intent_not_approved\"` (the approved request did not include this intent; file a change request), `reason: \"plan_upgrade_required\"` below Team, or not the owner. `code: \"agent_not_permitted\"`: a connected agent or an admin API key, at every scope (reads included). Requesting single sign-on or Directory Sync can commit Hookie to a recurring WorkOS cost, so it is done by a person in the console. Also refused before the route is reached: `Missing X-Requested-With header` (a session without it), or `reason: \"workspace_suspended\"` while the workspace is suspended.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "reason": {
                          "type": "string"
                        },
                        "status": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "502": {
            "description": "WorkOS could not create the link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "WorkOS is not configured in this environment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`Internal error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "session": []
          }
        ]
      }
    },
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Is the API up?",
        "description": "Public and unauthenticated (#323): whether this deployment can answer, from fast checks with no side effects \u2014 a trivial D1 read with a 2-second timeout, and whether the outbound queue is bound (nothing is sent). The body carries only `status`, `checks` and `time`: never a version, tenant data, a database error message or a secret. Every answer is `Cache-Control: no-store`. CORS: only the marketing origins (https://hookie.ai, https://www.hookie.ai and the preview site https://preview.hookie.ai) may read it from a browser \u2014 the request's Origin is echoed in Access-Control-Allow-Origin for those and no other, with `Vary: Origin` always and never with credentials. Rate limited per client IP (about 20 a minute, a best-effort guard counted separately at each Cloudflare location; fails open if the limiter is unavailable). `health` is a reserved root, so nothing under /health is ever read as /{workspace}/{project}/{webhook} ingest. HEAD returns the same status and headers with no body; OPTIONS answers a CORS preflight with 204.",
        "tags": [
          "Status"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Every check passed.",
            "headers": {
              "Access-Control-Allow-Origin": {
                "description": "The request's Origin, echoed only when it is https://hookie.ai, https://www.hookie.ai or https://preview.hookie.ai. Absent for any other origin. Credentials are never allowed.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "Always `Origin`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "Origin"
                  ]
                }
              },
              "Cache-Control": {
                "description": "Always `no-store`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "no-store"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "checks",
                    "time"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "degraded"
                      ],
                      "description": "`ok` when every check passed, `degraded` when any failed."
                    },
                    "checks": {
                      "type": "object",
                      "required": [
                        "database",
                        "queue"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "database": {
                          "type": "string",
                          "enum": [
                            "ok",
                            "error"
                          ],
                          "description": "A trivial read (`SELECT 1`) of the D1 database, failed if it errors or takes longer than 2 seconds."
                        },
                        "queue": {
                          "type": "string",
                          "enum": [
                            "ok",
                            "error"
                          ],
                          "description": "Whether the outbound delivery queue is bound. No message is sent."
                        }
                      }
                    },
                    "time": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the checks ran (UTC)."
                    }
                  }
                },
                "example": {
                  "status": "ok",
                  "checks": {
                    "database": "ok",
                    "queue": "ok"
                  },
                  "time": "2026-10-01T12:00:00.000Z"
                }
              }
            }
          },
          "503": {
            "description": "At least one check failed: `status: \"degraded\"`, and the failed check is `error`.",
            "headers": {
              "Access-Control-Allow-Origin": {
                "description": "The request's Origin, echoed only when it is https://hookie.ai, https://www.hookie.ai or https://preview.hookie.ai. Absent for any other origin. Credentials are never allowed.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "Always `Origin`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "Origin"
                  ]
                }
              },
              "Cache-Control": {
                "description": "Always `no-store`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "no-store"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status",
                    "checks",
                    "time"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "degraded"
                      ],
                      "description": "`ok` when every check passed, `degraded` when any failed."
                    },
                    "checks": {
                      "type": "object",
                      "required": [
                        "database",
                        "queue"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "database": {
                          "type": "string",
                          "enum": [
                            "ok",
                            "error"
                          ],
                          "description": "A trivial read (`SELECT 1`) of the D1 database, failed if it errors or takes longer than 2 seconds."
                        },
                        "queue": {
                          "type": "string",
                          "enum": [
                            "ok",
                            "error"
                          ],
                          "description": "Whether the outbound delivery queue is bound. No message is sent."
                        }
                      }
                    },
                    "time": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the checks ran (UTC)."
                    }
                  }
                },
                "example": {
                  "status": "degraded",
                  "checks": {
                    "database": "error",
                    "queue": "ok"
                  },
                  "time": "2026-10-01T12:00:00.000Z"
                }
              }
            }
          },
          "429": {
            "description": "Over the per-IP limit: {error, retry_after: 60}, with a Retry-After: 60 header.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 60.",
                "schema": {
                  "type": "integer"
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "The request's Origin, echoed only when it is https://hookie.ai, https://www.hookie.ai or https://preview.hookie.ai. Absent for any other origin. Credentials are never allowed.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "Always `Origin`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "Origin"
                  ]
                }
              },
              "Cache-Control": {
                "description": "Always `no-store`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "no-store"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "retry_after": {
                          "type": "integer",
                          "enum": [
                            60
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      },
      "head": {
        "operationId": "headHealth",
        "summary": "Is the API up? (headers only)",
        "description": "Public and unauthenticated (#323): whether this deployment can answer, from fast checks with no side effects \u2014 a trivial D1 read with a 2-second timeout, and whether the outbound queue is bound (nothing is sent). The body carries only `status`, `checks` and `time`: never a version, tenant data, a database error message or a secret. Every answer is `Cache-Control: no-store`. CORS: only the marketing origins (https://hookie.ai, https://www.hookie.ai and the preview site https://preview.hookie.ai) may read it from a browser \u2014 the request's Origin is echoed in Access-Control-Allow-Origin for those and no other, with `Vary: Origin` always and never with credentials. Rate limited per client IP (about 20 a minute, a best-effort guard counted separately at each Cloudflare location; fails open if the limiter is unavailable). `health` is a reserved root, so nothing under /health is ever read as /{workspace}/{project}/{webhook} ingest. HEAD returns the same status and headers with no body; OPTIONS answers a CORS preflight with 204.",
        "tags": [
          "Status"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Every check passed. No body.",
            "headers": {
              "Access-Control-Allow-Origin": {
                "description": "The request's Origin, echoed only when it is https://hookie.ai, https://www.hookie.ai or https://preview.hookie.ai. Absent for any other origin. Credentials are never allowed.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "Always `Origin`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "Origin"
                  ]
                }
              },
              "Cache-Control": {
                "description": "Always `no-store`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "no-store"
                  ]
                }
              }
            }
          },
          "503": {
            "description": "At least one check failed: `status: \"degraded\"`, and the failed check is `error`. No body.",
            "headers": {
              "Access-Control-Allow-Origin": {
                "description": "The request's Origin, echoed only when it is https://hookie.ai, https://www.hookie.ai or https://preview.hookie.ai. Absent for any other origin. Credentials are never allowed.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "Always `Origin`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "Origin"
                  ]
                }
              },
              "Cache-Control": {
                "description": "Always `no-store`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "no-store"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Over the per-IP limit: {error, retry_after: 60}, with a Retry-After: 60 header. No body.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying: 60.",
                "schema": {
                  "type": "integer"
                }
              },
              "Access-Control-Allow-Origin": {
                "description": "The request's Origin, echoed only when it is https://hookie.ai, https://www.hookie.ai or https://preview.hookie.ai. Absent for any other origin. Credentials are never allowed.",
                "schema": {
                  "type": "string"
                }
              },
              "Vary": {
                "description": "Always `Origin`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "Origin"
                  ]
                }
              },
              "Cache-Control": {
                "description": "Always `no-store`.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "no-store"
                  ]
                }
              }
            }
          }
        }
      },
      "options": {
        "operationId": "preflightHealth",
        "summary": "CORS preflight for /health",
        "description": "Answers 204 without running any check. For an allowed origin (https://hookie.ai, https://www.hookie.ai, https://preview.hookie.ai) it carries Access-Control-Allow-Origin, Access-Control-Allow-Methods: GET, HEAD and Access-Control-Max-Age: 600; any other origin gets no CORS header.",
        "tags": [
          "Status"
        ],
        "security": [],
        "responses": {
          "204": {
            "description": "Preflight answered."
          }
        }
      }
    },
    "/v1/agents/register/challenge": {
      "get": {
        "operationId": "getAgentRegistrationChallenge",
        "summary": "Get a proof-of-work challenge to register an agent",
        "tags": [
          "Agents"
        ],
        "security": [],
        "description": "Public: no cookie, no key. Issues a signed, stateless challenge that expires in ten minutes. Solve it by finding a `nonce` \u2014 a decimal string of at most 32 digits, found by counting up from 0 \u2014 such that SHA-256(`challenge` + `\":\"` + `nonce`) starts with `difficulty_bits` zero bits, then POST /v1/agents/register. The cost is small for one agent and large for a script creating accounts in bulk. `difficulty_bits` is the operator's setting (20 by default), plus 2, 4 or 6 bits once today's registrations pass 50, 75 or 90% of the daily cap: solve at the difficulty the challenge states. Refused when the operator has switched registration off.",
        "responses": {
          "200": {
            "description": "A challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "challenge",
                    "algorithm",
                    "difficulty_bits",
                    "expires_at",
                    "solve",
                    "register_url",
                    "terms_version",
                    "terms_url"
                  ],
                  "properties": {
                    "challenge": {
                      "type": "string",
                      "description": "`v1.<payload>.<mac>`, signed by Hookie. Send it back unchanged as `pow.challenge`. Single use: one challenge registers at most one account."
                    },
                    "algorithm": {
                      "type": "string",
                      "enum": [
                        "sha256"
                      ]
                    },
                    "difficulty_bits": {
                      "type": "integer",
                      "description": "Leading zero bits SHA-256(challenge + \":\" + nonce) must have. 20 by default (about a million hashes on average: one to three seconds of CPU with Node's crypto.createHash or Python's hashlib); the operator can set 12\u201326."
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Ten minutes after issue."
                    },
                    "solve": {
                      "type": "string",
                      "description": "The puzzle and the next request, in words."
                    },
                    "register_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "terms_version": {
                      "type": "string",
                      "description": "The current Terms of Use version (currently `2026-10-01`): the value `accept_terms` must have."
                    },
                    "terms_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Only GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this address (`reason: rate_limited`, the AGENT_RL limiter: about 5 a minute per IP, an IPv6 address counted by its /64, best-effort and counted per Cloudflare location), or, on POST, today's platform-wide registration cap is reached (`reason: daily_cap_reached`) or this network (an IPv4 address or an IPv6 /48) has made its share of it, a tenth (`reason: source_daily_limit`); both retry after midnight UTC.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "`agent_registration_disabled`: the operator has switched agent self-registration off. `agent_registration_unavailable`: this environment has no WorkOS API key, so the agent's identity cannot be made. On POST also `registration_incomplete`: retry the SAME request after `retry_after` seconds; it finishes the registration rather than making another.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/register": {
      "post": {
        "operationId": "registerAgent",
        "summary": "Register an AI agent's own Hookie account",
        "tags": [
          "Agents"
        ],
        "security": [],
        "description": "Public: no cookie, no key. Creates, all or nothing: the agent's identity in WorkOS (a User Management user with a non-deliverable, verified address on agents.hookie.ai, external_id = the account id; no WorkOS organization), its Hookie user, a workspace on the **Free plan** with its Default project and the agent as owner, an admin API key, and a one-time claim token. The key and the claim token are in this response and nowhere else: Hookie stores only their hashes, never logs them, and never returns them again.\n\n**Controls, in order:** a per-IP rate limit (an IPv6 address by its /64); the operator's kill switch; body validation including `accept_terms`; the proof of work (signed, unexpired, solved; harder as the day's cap fills); a platform-wide daily cap (default 200 a day, UTC) of which one network (an IPv4 address or an IPv6 /48) may take a tenth, both taken in the same statement that spends the challenge and before WorkOS is called. Free-plan quotas apply in full.\n\n**Idempotent.** A challenge registers at most one account. Sending the same request again \u2014 the same challenge, or the same `Idempotency-Key` header with a new, unexpired and solved challenge \u2014 never creates a second account: it answers 409 `already_registered` with the `account_id` and NO credential (the credential is returned once; if it was lost, register again with a new challenge and a new Idempotency-Key). A key is scoped to the request: the same key with a different `agent_name` or `operator_contact` is a different registration. While the first request is still running the answer is 409 `registration_in_progress`; a request that died part-way is finished by a retry of the SAME request (its original challenge) after a minute, adopting the WorkOS user it may already have made \u2014 a key with a new challenge never finishes it. One never retried is abandoned after an hour: its WorkOS user is deleted and the retry gets `challenge_spent`. An attempt that failed (502) releases its key, so a new challenge with the same key registers.\n\n**What the key may do.** Everything an admin can \u2014 projects, endpoints, rules, destinations, workflows, ingest keys, the rest \u2014 through /admin/api/*, the hosted MCP server at /mcp and the CLI. Billing, SSO and organization requests, invites, Google Workspace linking, platform administration, creating more API keys and revealing destination secrets answer 403 `agent_not_permitted` (with `claim_required: true`, except platform administration) until a person claims the workspace (and billing stays a person's in the console after that). Audited as `agent_registered`.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Optional, 1\u2013255 printable characters. A retry carrying the same key (with the same `agent_name` and `operator_contact`, and a live solved challenge) is answered 409 with the account it created, never a second account. A key whose attempt failed is released. Use a random value such as a UUID.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "agent_name",
                  "accept_terms",
                  "pow"
                ],
                "properties": {
                  "agent_name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80,
                    "description": "What the agent calls itself. Printable characters only. Names the workspace (`<agent_name>'s workspace`)."
                  },
                  "operator_contact": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Optional: the email address or https URL of the person or organization responsible for the agent. Stored on the account and shown to the operator."
                  },
                  "accept_terms": {
                    "type": "string",
                    "description": "Must equal the current Terms of Use version, `2026-10-01` (also in the challenge response as `terms_version`). By sending it the agent accepts the Terms at https://hookie.ai/legal/terms for the person or organization it acts for; the version is recorded on the account."
                  },
                  "pow": {
                    "type": "object",
                    "required": [
                      "challenge",
                      "nonce"
                    ],
                    "properties": {
                      "challenge": {
                        "type": "string",
                        "description": "From GET /v1/agents/register/challenge, unchanged."
                      },
                      "nonce": {
                        "type": "string",
                        "pattern": "^[0-9]{1,32}$",
                        "description": "The solution."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered. Store `api_key.key` and `claim.token` now: they are never shown again.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "account_id",
                    "agent_name",
                    "user_id",
                    "workspace",
                    "project",
                    "api_key",
                    "claim",
                    "terms_version",
                    "endpoints",
                    "notice"
                  ],
                  "properties": {
                    "account_id": {
                      "type": "string",
                      "description": "`agt_\u2026`. The agent account."
                    },
                    "agent_name": {
                      "type": "string"
                    },
                    "user_id": {
                      "type": "string",
                      "description": "The agent's WorkOS user id, also its Hookie user id."
                    },
                    "workspace": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "slug",
                        "plan"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        },
                        "plan": {
                          "type": "string",
                          "enum": [
                            "free"
                          ]
                        }
                      }
                    },
                    "project": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "slug"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        }
                      }
                    },
                    "api_key": {
                      "type": "object",
                      "required": [
                        "id",
                        "key",
                        "key_prefix",
                        "role"
                      ],
                      "description": "The agent's credential: an admin API key, shown here ONCE and stored only as its SHA-256. Send it as `Authorization: Bearer <key>` to /admin/api/* and /mcp, or set HOOKIE_TOKEN for the CLI. Listed under Settings \u2192 API keys as \"Agent registration key\".",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "key": {
                          "type": "string",
                          "description": "`hk_\u2026`"
                        },
                        "key_prefix": {
                          "type": "string"
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "admin"
                          ]
                        }
                      }
                    },
                    "claim": {
                      "type": "object",
                      "required": [
                        "token",
                        "url",
                        "note"
                      ],
                      "description": "A one-time token for the person responsible for the agent, shown here ONCE and stored only as its SHA-256. Signed in to Hookie, they open `url` (or POST the token to /admin/api/agent-accounts/claim) to become the workspace's owner \u2014 which they need to upgrade it.",
                      "properties": {
                        "token": {
                          "type": "string",
                          "description": "`hkc_\u2026`"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    },
                    "terms_version": {
                      "type": "string"
                    },
                    "endpoints": {
                      "type": "object",
                      "properties": {
                        "api": {
                          "type": "string",
                          "format": "uri"
                        },
                        "mcp": {
                          "type": "string",
                          "format": "uri"
                        },
                        "openapi": {
                          "type": "string",
                          "format": "uri"
                        },
                        "docs": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    },
                    "usage": {
                      "type": "string"
                    },
                    "notice": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is wrong (`invalid_body`, `invalid_agent_name`, `invalid_operator_contact`), the Terms version is missing or old (`terms_not_accepted`, with the current `terms_version`), the proof of work is missing, forged, expired or wrong (`proof_of_work_required`, `invalid_challenge`, `challenge_expired`, `invalid_proof_of_work`), or the Idempotency-Key is malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Only POST.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A retry: `already_registered` (with `account_id` and `workspace_id`, no credential), `registration_in_progress` (retry the original request, with its original challenge, after `retry_after`), or `challenge_spent` (an earlier attempt with this challenge failed; get a new one).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "account_id": {
                          "type": "string"
                        },
                        "workspace_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "413": {
            "description": "The body is over 4 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`identity_provider_error`: WorkOS could not create the identity. Nothing was registered and the challenge is spent; get a new one and try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests from this address (`reason: rate_limited`, the AGENT_RL limiter: about 5 a minute per IP, an IPv6 address counted by its /64, best-effort and counted per Cloudflare location), or, on POST, today's platform-wide registration cap is reached (`reason: daily_cap_reached`) or this network (an IPv4 address or an IPv6 /48) has made its share of it, a tenth (`reason: source_daily_limit`); both retry after midnight UTC.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. Same value as retry_after in the body.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "`agent_registration_disabled`: the operator has switched agent self-registration off. `agent_registration_unavailable`: this environment has no WorkOS API key, so the agent's identity cannot be made. On POST also `registration_incomplete`: retry the SAME request after `retry_after` seconds; it finishes the registration rather than making another.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/admin/api/agent-accounts/claim-preview": {
      "post": {
        "operationId": "previewAgentClaim",
        "summary": "See what a claim token would take over",
        "tags": [
          "Workspace"
        ],
        "security": [
          {
            "session": []
          }
        ],
        "description": "Read-only: the agent account and workspace the token claims (#331). Nothing is written. Any signed-in person, in the console; never an OAuth agent or an API key (403 `agent_not_permitted`). Resolved WITHOUT the automatic workspace every other admin route creates for someone who has none, as accepting an invite is. A token that is wrong or already used answers 404 with the same words.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "claim_token"
                ],
                "properties": {
                  "claim_token": {
                    "type": "string",
                    "description": "`hkc_\u2026`, from the registration response (or the `#/claim/{token}` link)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What claiming would take over.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "account",
                    "workspace",
                    "suspended"
                  ],
                  "properties": {
                    "account": {
                      "type": "object",
                      "required": [
                        "id",
                        "name"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "workspace": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "slug"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        }
                      }
                    },
                    "suspended": {
                      "type": "boolean",
                      "description": "True when an operator suspended the workspace: it cannot be claimed until reinstated."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "An OAuth agent or API key (`agent_not_permitted`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The token is not valid, or was used already.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Only POST.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/admin/api/agent-accounts/claim": {
      "post": {
        "operationId": "claimAgentAccount",
        "summary": "Take ownership of a workspace an AI agent registered",
        "tags": [
          "Workspace"
        ],
        "security": [
          {
            "session": []
          }
        ],
        "description": "The person becomes the workspace's owner and it becomes their active workspace (#331). The agent identity's membership ends and the agent's API keys are re-pointed at the person, so the agent's key keeps working at its admin role, now acting for them (revoke it in Settings \u2192 API keys to cut the agent off). The Free plan's one-member limit holds. From then on the workspace is an ordinary one: billing, SSO requests, invites and the rest are the owner's. The token works once (two claims racing for it make one owner) and is audited as `agent_account_claimed`. A workspace an operator suspended answers 403 `workspace_suspended`. Any signed-in person, in the console; never an OAuth agent or an API key (403 `agent_not_permitted`). Resolved WITHOUT the automatic workspace every other admin route creates for someone who has none, as accepting an invite is. A token that is wrong or already used answers 404 with the same words.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "claim_token"
                ],
                "properties": {
                  "claim_token": {
                    "type": "string",
                    "description": "`hkc_\u2026`, from the registration response (or the `#/claim/{token}` link)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Claimed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "claimed",
                    "account",
                    "workspace",
                    "role"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "claimed": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "account": {
                      "type": "object",
                      "required": [
                        "id",
                        "name"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    },
                    "workspace": {
                      "type": "object",
                      "required": [
                        "id",
                        "name",
                        "slug"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "slug": {
                          "type": "string"
                        }
                      }
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "owner"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "An OAuth agent or API key (`agent_not_permitted`), or the workspace is suspended (`workspace_suspended`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The token is not valid, or was used already.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Only POST.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "delivery": {
      "post": {
        "operationId": "delivery",
        "summary": "An outbound delivery to a destination",
        "tags": [
          "Delivery"
        ],
        "description": "What Hookie POSTs to a destination URL for each routed record. Delivery is at-least-once and unordered: dedupe on Hookie-Event-Id (the same value as Idempotency-Key). Redirects are never followed. Every request is signed TWICE over one timestamp, so a receiver can verify whichever it prefers: Hookie-Signature (Stripe-style, t=<unix>,v1=<hex>) and the Standard Webhooks headers webhook-id / webhook-timestamp / webhook-signature, which any Standard Webhooks library verifies unchanged. For 24 hours after a signing-secret rotation (until the destination's previous_secret_expires_at) each header carries two signatures, the new secret's first: two v1= values in Hookie-Signature and two space-separated v1,<base64> entries in webhook-signature. Accept the request if ANY signature matches a secret you hold, and reject a timestamp more than five minutes from now. The two schemes derive DIFFERENT HMAC keys from the same whsec_ signing secret: Hookie-Signature is keyed with the UTF-8 bytes of the whole secret string, whsec_ included; webhook-signature is keyed, as the Standard Webhooks spec says, with the base64-decoded bytes of what follows whsec_. Hookie's secrets are whsec_ + 48 hex characters, which is valid base64 (hex digits are base64 characters and 48 is a multiple of 4) and decodes to 36 bytes, so any Standard Webhooks library accepts the secret exactly as shown.",
        "parameters": [
          {
            "name": "Hookie-Signature",
            "in": "header",
            "required": true,
            "description": "t=<unix seconds>,v1=<lowercase hex HMAC-SHA256 of \"<t>.<raw body>\">, keyed with the UTF-8 bytes of the whole signing secret (whsec_ included). During a rotation overlap: t=<unix>,v1=<new>,v1=<previous>. Unchanged since before the Standard Webhooks headers were added.",
            "schema": {
              "type": "string"
            },
            "example": "t=1790000000,v1=5f2b8c0d6e1a4b3c2d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c"
          },
          {
            "name": "webhook-id",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks message id: the delivery id (the same value as Hookie-Delivery-Id). Every retry of a delivery repeats it; a replay is a new delivery and carries a new one.",
            "schema": {
              "type": "string"
            },
            "example": "4c7b2a9e-1f3d-4e5a-8b6c-0d9e8f7a6b5c"
          },
          {
            "name": "webhook-timestamp",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks timestamp: unix seconds, the same value as Hookie-Signature's t.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            },
            "example": "1790000000"
          },
          {
            "name": "webhook-signature",
            "in": "header",
            "required": true,
            "description": "Standard Webhooks signature: v1,<base64 HMAC-SHA256 of \"<webhook-id>.<webhook-timestamp>.<raw body>\">, keyed with the base64-decoded bytes after whsec_ (for a Hookie secret, 36 bytes). During a rotation overlap two entries, space-separated, new first: v1,<new> v1,<previous>. Omitted, with webhook-id and webhook-timestamp, only for a secret that is not valid base64, which Hookie never mints.",
            "schema": {
              "type": "string"
            },
            "example": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="
          },
          {
            "name": "Hookie-Event-Id",
            "in": "header",
            "required": true,
            "description": "The record id: the same on every retry and replay. Dedupe on this.",
            "schema": {
              "type": "string"
            },
            "example": "rec_9d3f2a10"
          },
          {
            "name": "Hookie-Delivery-Id",
            "in": "header",
            "required": true,
            "description": "This delivery's id (the same value as webhook-id).",
            "schema": {
              "type": "string"
            },
            "example": "4c7b2a9e-1f3d-4e5a-8b6c-0d9e8f7a6b5c"
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "The record id, for receivers that dedupe on this header.",
            "schema": {
              "type": "string"
            },
            "example": "rec_9d3f2a10"
          },
          {
            "name": "Hookie-Hop",
            "in": "header",
            "required": true,
            "description": "How many times this event has passed through Hookie, plus one. Hookie refuses an event whose hop has reached 8 (508); a relay that forwards into Hookie should pass it on.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]+$"
            },
            "example": "1"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeliveryEnvelope"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Delivered."
          },
          "3XX": {
            "description": "Not followed: a failed attempt that is not retried; the delivery's error names the Location."
          },
          "4XX": {
            "description": "Refused: not retried, except 404, 408, 409, 425 and 429, which are. A 429 Retry-After is honoured."
          },
          "5XX": {
            "description": "Retried with backoff (about 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h; eight attempts). A 503 Retry-After is honoured."
          }
        }
      }
    }
  }
}
