> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hookie.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Create an endpoint

> 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.



## OpenAPI

````yaml /openapi.json post /admin/api/projects/{project_id}/webhooks
openapi: 3.1.0
info:
  title: Hookie API
  version: 1.0.0
  summary: Capture, route and deliver webhooks.
  description: >-
    Hookie exposes three HTTP surfaces.


    **Ingest + streaming** is the public plane. A path-form endpoint URL
    authenticates by its own high-entropy slug — there is no key, token or
    cookie — and an `ik_live_…` 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.


    **The customer portal API** is a separate, token-authed surface for your end
    customers, scoped to one portal and one customer.


    **The admin API** drives the console. It accepts either a browser session or
    an OAuth bearer token from a connected coding agent — 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_…`, Settings → 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 — 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`.


    **A refused agent is told how to proceed.** An agent's grant is one rung of
    a ladder — `hookie:read` < `hookie:write` < `hookie:manage` — 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`.


    **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 — console, OAuth
    agent or hosted MCP alike — is refused except billing, platform admin,
    connected-agent management, revoking an API key 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.


    **Rate limits.** `/admin/api/*` and `/mcp` allow about 100 requests every 10
    seconds per credential — 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.


    **Idempotency.** A `POST` to `/admin/api/*` may carry an `Idempotency-Key`
    header (1–255 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.


    **Versioning.** Every `/admin/api/*` and `/mcp` response carries
    `Hookie-Version` (currently `2026-09-29`). Additive changes — a new route, a
    new response field, a new optional parameter — 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`.


    This 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
security: []
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, admin API keys, data export and
      closure. There are no member invites or member routes yet (#258): a
      workspace has its owner, plus any admin the platform sets up.
  - 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.
paths:
  /admin/api/projects/{project_id}/webhooks:
    post:
      tags:
        - Webhooks
      summary: Create an endpoint
      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.
      operationId: createWebhook
      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).
      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 — 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 — 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 — 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 — 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 — 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 → 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'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: Internal error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - session: []
        - bearerAuth: []
        - apiKey: []
components:
  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
  schemas:
    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
    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.
    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 only on a monthly-quota 429.
        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.
        code:
          type: string
          enum:
            - agent_scope_insufficient
            - insufficient_role
            - agent_not_permitted
          description: >-
            Present on a 403 to an OAuth-connected agent.
            `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 — and
            roles cannot be changed in the product yet, so the call is one for
            the workspace owner. `agent_not_permitted`: no connected agent may
            do this at any scope (billing, platform admin, managing connected
            agents, a destination's signing secret).
        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
            → Connected agents page, opened on this agent
            (`…/#/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.'
  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'
  securitySchemes:
    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 —
        a bearer token is not ambient credentials.
    apiKey:
      type: http
      scheme: bearer
      description: >-
        An `hk_…` admin API key from Settings → 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.

````