openapi: 3.0.3
info:
  title: Extrovert API
  version: "0.1.0-pre.6"
  description: >
    Extrovert gives an AI agent a persistent inbox that sends and receives mail.
    Create it in one call with an agent key whose permissions can expire or be revoked.


    The surface has two planes:


    - **Agent plane** (`/v1/...`, excluding `/v1/admin`) uses a
      permissioned agent key (`pk_agent_...`) as a Bearer token. `customer_id` is always
      derived from the key, never from client input, so the API is tenant-scoped by
      construction. The MCP server and TypeScript SDK map 1:1 onto this plane.

    - **Console / admin plane** (`/v1/admin/...`) uses a Clerk
      session token and strictly customer-scoped. Backs the human console,
      including the shared-inbox viewer.


    `POST /v1/enroll` and `POST /v1/agent/sign-up` are unauthenticated bootstrap
    endpoints. `GET /healthz`, `GET /readyz`, `GET /openapi.yaml`, and
    `GET /openapi.json` are public.


    **Review Loop contract.** `info.version`, the SDK package version, and the SDK
    `CONTRACT_VERSION` all use `0.1.0-pre.6`. The stable agent-facing Review Loop
    JSON shapes are published in the SDK `contract` module (`CONTRACT_MANIFEST`)
    and the Agent contract documentation.
  license:
    name: Proprietary
servers:
  - url: https://api.extrovert.dev
    description: Production
  - url: http://localhost:8090
    description: Local
tags:
  - name: ops
    description: Health probes and the machine-readable API description.
  - name: enrollment
    description: Bootstrap an agent identity from an enrollment token or self-signup.
  - name: inboxes
    description: Create, list, get, update, and delete inboxes. Paid plans can also fetch mail-client credentials.
  - name: messaging
    description: Send, reply, forward, read messages and threads, and wait for inbound mail.
  - name: attachments
    description: List and download message attachments.
  - name: webhooks
    description: Register HMAC-signed inbound delivery webhooks.
  - name: domains
    description: >
      Agent-facing domain management (privileged, domain:manage scope): list,
      get, onboard, verify, and offboard the customer's domains over the shared
      onboarding infrastructure.
  - name: commerce
    description: Agent-safe quotes and human-controlled purchase or plan-change requests.
  - name: admin
    description: Clerk-authenticated console / admin plane (customer-scoped).
  - name: billing
    description: Extrovert subscription billing and Stripe webhook ingestion.
  - name: unsubscribe
    description: >
      Public, unauthenticated List-Unsubscribe endpoints (RFC 8058 one-click POST
      and the preferences page) backing recipient-driven suppression.
  - name: suppressions
    description: >
      Recipient opt-out (suppression) management: the agent-plane pre-check + list +
      revoke and the console/admin list/search/export/revoke. Reads and revoke are
      strictly org-scoped; global/shared-domain rows are operator-only and never
      exposed.

components:
  securitySchemes:
    AgentKey:
      type: http
      scheme: bearer
      description: >
        Per-agent capability key (`pk_agent_...`) presented as
        `Authorization: Bearer ...`. Scoped (mailbox:create / read / send /
        delete / quota / credentials; signup:verify; domain:manage;
        domain:purchase; commerce:request; review:act), revocable, and
        self-expiring. The customer/tenant and the fixed org/project binding are
        derived from the key, never from client input. Privileged
        scopes are opt-in and default-off: `domain:manage` gates onboarding for
        domains the customer already controls. `commerce:request` permits quotes,
        durable purchase/plan requests, and status reads but never approval or spend.
    ClerkSession:
      type: http
      scheme: bearer
      description: >
        Clerk session token for the console/admin surface. Authority is derived
        from the identity's membership in the org/project addressed in the PATH
        (/v1/admin/orgs/{org_id}/projects/{project_id}/...); there are no scope
        headers.

  parameters:
    Addr:
      in: path
      name: addr
      required: true
      description: The full inbox address, URL-encoded (e.g. `agent7%40extrovertmail.com`).
      schema: { type: string }
    InboxID:
      in: path
      name: inbox_id
      required: true
      description: >
        Opaque inbox id (the canonical path key). The inbox's email address is also
        accepted here as a within-project alias. Treat the id as opaque.
      schema: { type: string }
    MessageID:
      in: path
      name: id
      required: true
      description: Opaque message id (`msg_...`). Encodes its owning inbox.
      schema: { type: string }
    AttachmentID:
      in: path
      name: attId
      required: true
      description: Opaque attachment id locating one MIME part of the message.
      schema: { type: string }
    Limit:
      in: query
      name: limit
      required: false
      description: Page size for the cursor-paginated agent surface (default 50; clamped to the 1-100 range).
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    Offset:
      in: query
      name: offset
      required: false
      description: Result offset for the legacy offset-paginated surface.
      schema: { type: integer, minimum: 0 }
    Cursor:
      in: query
      name: cursor
      required: false
      description: >
        Opaque pagination cursor echoed back from a prior `next_cursor`. Treat it as
        an opaque token; pass it back verbatim as `?cursor` to fetch the next page.
      schema: { type: string }
    Include:
      in: query
      name: include
      required: false
      description: >
        Comma-separated relation expansions (e.g. `?include=agent,domain`). Each
        relation is validated against a per-resource allowlist (depth <= 2); a join
        re-applies the same ceiling/ownership filter as a direct read. A disallowed or
        too-deep path is a 400 `bad_request`.
      schema: { type: string }
    PageToken:
      in: query
      name: page
      required: false
      description: >
        Opaque page token echoed back from a prior `next_page`. Used by the legacy
        offset-paginated admin/console list surface; the cursor-paginated agent
        surface uses `Cursor` (`?cursor`) instead.
      schema: { type: string }
    Folder:
      in: query
      name: folder
      required: false
      description: IMAP folder to read (default INBOX).
      schema: { type: string }
    Domain:
      in: path
      name: domain
      required: true
      description: The fully-qualified domain name (e.g. `mail.acme.com`).
      schema: { type: string }
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: false
      description: >
        Optional client-supplied key making a CREATE exactly-once. A retry with the
        same key returns the ORIGINAL response (same status + body) instead of
        creating a duplicate; the same key with a different request body returns
        409. The body field `client_id` is honored as an alias when this header is
        absent. The key is scoped per tenant (and per agent on the agent plane), so
        keys never collide across callers.
      schema: { type: string }
    LastEventID:
      in: header
      name: Last-Event-ID
      required: false
      description: >
        SSE resume token. On reconnect, set this to the `id` of the last event you
        received and the server replays every event after it. EventSource sets this
        header automatically; non-browser clients may instead pass the
        `last_event_id` query parameter.
      schema: { type: string }
    LastEventIDQuery:
      in: query
      name: last_event_id
      required: false
      description: >
        SSE resume token as a query parameter, for clients (curl, fetch) that cannot
        set the `Last-Event-ID` header. Same semantics: replay events after this id.
      schema: { type: string }

  schemas:
    List:
      type: object
      description: >
        The ONE collection envelope for every list response (redesign §5.2).
        `next_cursor` is an opaque pagination token (pass it back as `?cursor`);
        it is null when there are no more rows.
      required: [object, data, has_more]
      properties:
        object: { type: string, enum: [list] }
        data: { type: array, items: {} }
        has_more: { type: boolean }
        next_cursor: { type: string, nullable: true }
    Problem:
      type: object
      description: >
        RFC-9457 problem+json error body. `code` is a closed machine enum clients
        switch on; `type` is a dereferenceable URI under
        https://extrovert.dev/problems/. Served as application/problem+json.
      required: [type, title, status, code]
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        code:
          type: string
          description: >
            The CLOSED machine code. The Review Loop members split what used to be a
            single opaque `conflict`, because an agent must take a DIFFERENT action on
            each: `stale` (the revision/version you named is no longer current; nothing
            was mutated; re-read, re-apply, resubmit; retryable, bounded) and
            `born_stale` (built against an older rule high-water; re-read the rules or
            restamp_review; at most one retry per high-water) are the ONLY retryable
            409s. `wrong_state` means this VERB is illegal from the current state while
            the draft is still live; never retry the same verb, read the `state` and
            the repeated `allowed_action` hints in `errors[]` and pick a legal one.
            `terminal` means the review is already sent/auto_sent/cancelled and nothing
            will EVER succeed; stop, and drain your review events for the outcome.
            `send_needs_reconciliation` means a prior send is unconfirmed and parked.
            Do not resend. Poll instead. `unavailable` (503) is the retryable fail-closed
            answer when a dependency could not be read; it carries `Retry-After` and is
            distinct from `not_configured`, which is permanent for this deployment.
          enum: [bad_request, unauthorized, forbidden_scope, not_found, conflict,
                 idempotency_conflict, breadth_required, quota_exceeded, rate_limited,
                 domain_not_allowed, recipient_blocked, recipient_suppressed,
                 not_configured, domain_unavailable, internal,
                 intent_required, wrong_state, terminal, stale, born_stale,
                 send_needs_reconciliation, graduation_locked, maturity_gate_unmet,
                 scope_taken, unavailable]
        request_id: { type: string }
        errors:
          type: array
          items:
            type: object
            properties:
              field: { type: string }
              code: { type: string }
              detail: { type: string }
    Error:
      type: object
      description: The canonical error envelope. `error` is a stable machine code.
      required: [error]
      properties:
        error:
          type: string
          description: Stable error code (e.g. `unauthorized`, `forbidden`, `not_found`, `invalid`, `quota_exceeded`, `rate_limited`).
          example: forbidden
        message:
          type: string
          description: Human-readable detail (never leaks internals).
          example: missing required scope

    SignupStatus:
      type: object
      required: [free_signups_enabled]
      properties:
        free_signups_enabled:
          type: boolean
          description: Whether unauthenticated free-account signup and verification are operationally enabled. Currently false in production.

    EmailAddress:
      type: object
      description: A structured RFC5322 address.
      required: [email]
      properties:
        name: { type: string, example: "Acme Support" }
        email: { type: string, format: email, example: support@acme.test }

    Connection:
      type: object
      description: One IMAP or SMTP endpoint for a mailbox.
      properties:
        host: { type: string, example: smtp.extrovert.dev }
        port: { type: integer, example: 993 }
        security:
          type: string
          description: '`tls` (implicit TLS, e.g. IMAP 993) or `starttls` (e.g. SMTP 587).'
          enum: [tls, starttls]

    Inbox:
      type: object
      description: >
        An inbox. `id` is the canonical opaque inbox id and the path key
        (`/v1/inboxes/{inbox_id}`); treat it as an opaque string. `address` is the
        within-project email alias. Every redesign resource carries `object`,
        `org_id`, `project_id`, and timestamps (RFC D9). `password` is returned only
        on an explicitly requested, paid create response whose key has
        `mailbox:credentials`; list/get responses omit it.
      properties:
        object: { type: string, enum: [inbox] }
        id: { type: string, description: "Opaque inbox id (the path key). Treat as opaque.", example: pmbx_8f3c2a1b }
        org_id: { type: string, example: org_1f2e }
        project_id: { type: string, example: prj_9a8b }
        address: { type: string, example: support@extrovertmail.com }
        agent_id: { type: string, example: agent_3kP9wQ }
        display_name: { type: string, example: Support Bot }
        password:
          type: string
          description: Mailbox login. Present only on the create response; never persisted in the clear.
        smtp_host: { type: string }
        smtp_port: { type: integer, example: 587 }
        imap_host: { type: string }
        imap_port: { type: integer, example: 993 }
        webhook_url: { type: string, description: Inbound webhook registered for this inbox, if any. }
        daily_send_limit:
          type: integer
          minimum: 1
          maximum: 10000
          description: Effective rolling-24h recipient cap enforced for this inbox.
        direct_smtp_enabled:
          type: boolean
          readOnly: true
          description: >
            Whether this inbox is configured for direct SMTP submission outside the
            review pipeline. It is disabled by default, read-only for agents, and can
            be enabled only by a human administrator for an account with an active
            paid entitlement. The stored toggle does not grant access after that paid
            entitlement ends.
        metadata:
          type: object
          description: >
            Arbitrary key-value metadata stored on the inbox (AgentMail parity).
            Always an object; `{}` when none is set, never null. Values are
            string, number, or boolean. Project-scoped: an agent key can only read
            or mutate metadata for inboxes in its bound project.
          additionalProperties:
            oneOf:
              - { type: string }
              - { type: number }
              - { type: boolean }
        effective_review_policy:
          type: string
          enum: [require_review, allow_direct, auto_send_graduated]
          description: >
            The RESOLVED review policy for this inbox: the per-inbox override, else
            the account default, else the `require_review` floor. Read it once before
            your first send: under `require_review` a send/reply/forward WITHOUT an
            `intent` is rejected 422 `intent_required` (nothing sent, nothing queued),
            and one WITH an intent is queued for a human (202 `queued_for_review`).
            Present on the single-inbox GET only; the list response omits it.
        created_at: { type: string, format: date-time }

    InboxCredentials:
      type: object
      description: >
        Full IMAP/SMTP connection config and login for an owned inbox. Export requires
        a paid plan plus `mailbox:credentials`. IMAP remains usable after export, but
        SMTP submission is rejected unless a human has separately enabled the inbox's
        paid-only `direct_smtp_enabled` bypass.
      properties:
        address: { type: string, example: support@extrovertmail.com }
        username: { type: string, description: Equals the address. }
        password: { type: string }
        imap: { $ref: "#/components/schemas/Connection" }
        smtp: { $ref: "#/components/schemas/Connection" }

    Message:
      type: object
      description: The canonical message wire shape (agent plane).
      properties:
        id: { type: string, example: msg_8Tz, description: Opaque, inbox-resolvable id. }
        thread_id: { type: string, example: thr_9aB }
        inbox: { type: string, example: agent7@extrovertmail.com }
        direction:
          type: string
          enum: [inbound, outbound]
          description: Derived relative to the owning inbox.
        from: { $ref: "#/components/schemas/EmailAddress" }
        to:
          type: array
          items: { $ref: "#/components/schemas/EmailAddress" }
        cc:
          type: array
          items: { $ref: "#/components/schemas/EmailAddress" }
        reply_to:
          type: array
          items: { $ref: "#/components/schemas/EmailAddress" }
        subject: { type: string }
        text:
          type: string
          nullable: true
          description: Decoded UTF-8 text/plain MIME alternative; null when the message had no plain-text part. Never derived from HTML.
        html:
          type: string
          nullable: true
          description: Decoded text/html MIME alternative; null for text-only mail. Never synthesized from plain text.
        extracted_text:
          type: string
          nullable: true
          description: Best-effort derivative of text with quoted history and signatures removed; null when text is absent or no authored text remains. Not authoritative.
        extracted_html:
          type: string
          nullable: true
          description: Best-effort derivative of html with common quoted-history containers removed; null when html is absent or no authored HTML remains. Not authoritative.
        date: { type: string, description: Original Date header value. }
        message_id: { type: string, description: RFC5322 Message-ID. }
        in_reply_to: { type: string, description: RFC5322 In-Reply-To header. }
        references: { type: string, description: RFC5322 References header chain. }
        folder: { type: string }
        seen: { type: boolean, description: Native IMAP \Seen flag (read state). }

    MessagePage:
      type: object
      description: A paginated page of messages.
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/Message" }
        total: { type: integer }
        next_cursor:
          type: string
          description: Present when more results remain; pass the opaque value back as `cursor`.

    Thread:
      type: object
      description: A conversation backed by the mail server's persistent thread index.
      properties:
        id: { type: string, example: thr_9aB }
        inbox_id: { type: string, example: agent7@extrovertmail.com }
        subject: { type: string }
        participants:
          type: array
          items: { type: string }
        message_count: { type: integer }
        last_message_at: { type: string }
        snippet: { type: string }
        unread: { type: boolean, description: Whether the latest message is unread. }
        last_message_id: { type: string, description: Opaque id of the latest message; usable by message endpoints. }
        last_message_has_attachments: { type: boolean, description: Whether the latest message has attachments. }

    ThreadDetail:
      allOf:
        - $ref: "#/components/schemas/Thread"
        - type: object
          properties:
            messages:
              type: array
              description: The thread's messages, oldest-first.
              items: { $ref: "#/components/schemas/Message" }

    ThreadPage:
      type: object
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/Thread" }
        total: { type: integer }
        next_cursor: { type: string }

    Attachment:
      type: object
      description: Attachment metadata. Download the bytes from the attachment endpoint.
      properties:
        id: { type: string }
        filename: { type: string }
        content_type: { type: string, example: application/pdf }
        size: { type: integer, description: Size in bytes. }

    AttachmentPage:
      type: object
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/Attachment" }
        total: { type: integer }

    AttachmentInput:
      type: object
      description: An outbound attachment on send/reply/forward.
      required: [content_base64]
      properties:
        filename: { type: string }
        content_type: { type: string }
        content_base64:
          type: string
          description: Standard base64-encoded attachment bytes.

    SendRequest:
      type: object
      required: [to]
      properties:
        to:
          type: array
          items: { type: string, format: email }
          minItems: 1
        subject: { type: string }
        text:
          type: string
          description: >
            Plain-text body. This is the canonical name and matches
            ReplyRequest.text, ForwardRequest.text and Message.text.
        body:
          type: string
          deprecated: true
          description: >
            DEPRECATED alias for `text`, kept accepted indefinitely so
            already-deployed callers keep working; it will never be removed. Send
            `text` in new code. Supplying BOTH with different content is a 400
            (code bad_request, errors[].code conflicting_alias); the server never
            guesses which bytes to relay; supplying both with identical content is
            accepted.
        html: { type: string, description: Optional HTML body, sent as an alternative alongside the plain-text part. }
        cc:
          type: array
          items: { type: string, format: email }
        bcc:
          type: array
          items: { type: string, format: email }
        reply_to: { type: string, description: Override the Reply-To header. }
        headers:
          type: object
          additionalProperties: { type: string }
          description: Arbitrary custom headers (reserved/unsafe header names are dropped).
        in_reply_to: { type: string, description: Message-ID to thread under (optional). }
        attachments:
          type: array
          items: { $ref: "#/components/schemas/AttachmentInput" }
        mode:
          type: string
          enum: [review, direct]
          description: >
            Review Loop (HITL) per-send assertion. `review` (default) routes the
            message into the human-review queue; `direct` requests an immediate
            send. The authoritative account/inbox review policy may downgrade
            `direct` to `review`. Setting any of mode/intent/category_id opts the
            send into the Review Loop.
        intent:
          $ref: "#/components/schemas/ReviewIntent"
        category_id:
          type: string
          description: Opaque category id (cat_…) matched from the registry. Never a name.
        category_confidence:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: >
            Agent-supplied confidence (0..1) in the category match. Feeds the
            submit-time min_confidence auto-send gate ONLY; the server never scores
            ($0 LLM). Below the effective threshold (or omitted when a threshold is
            set) the would-be auto-send routes to needs_review (gate_outcome
            held:low_confidence).
        composition_token:
          type: string
          description: Opaque token from a fresh, unfiltered GET /v1/rules for this agent, project, and category.
        idempotency_key:
          type: string
          deprecated: true
          description: >
            DEPRECATED body-level alias for the `Idempotency-Key` header, tolerated
            for @extrovert.dev/sdk <= 0.1.0 builds that serialize the whole request
            object. Send the header instead; when both are present the header wins.
            Note that the replay key is scoped to a hash of the RAW request body, so
            the same logical message retried once as `body` and once as `text` under
            one key is a body mismatch and answers 409 idempotency_conflict. Different
            bytes are a different request.

    ReviewIntent:
      type: object
      description: >
        The agent's "for the human reviewer" summary. `summary` is REQUIRED when the
        resolved mode is review (else the submit is 422).
      properties:
        summary: { type: string, description: Free-text intent summary (who/what/why). }
        meta:
          type: object
          description: Structured intent payload.
          properties:
            goal: { type: string }
            recipient: { type: string }
            prior_touches: { type: integer }
            urgency: { type: string }

    ReplyRequest:
      type: object
      description: >
        Thread-aware reply. Exactly one of `thread_id` / `message_id` selects the
        parent; recipients, subject, and threading headers are derived server-side.
      oneOf:
        - required: [thread_id]
        - required: [message_id]
      properties:
        thread_id: { type: string }
        message_id: { type: string }
        text: { type: string }
        html: { type: string }
        cc:
          type: array
          items: { type: string, format: email }
        bcc:
          type: array
          items: { type: string, format: email }
        reply_to: { type: string }
        headers:
          type: object
          additionalProperties: { type: string }
        reply_all:
          type: boolean
          description: Include all original recipients (reply-all) when true.
        expected_last_message_id:
          type: string
          description: Optional stale-context guard; returns 409 if the thread head changed before request materialization.
        attachments:
          type: array
          items: { $ref: "#/components/schemas/AttachmentInput" }
        mode:
          type: string
          enum: [review, direct]
          description: Review Loop per-send assertion (see SendRequest.mode).
        intent:
          $ref: "#/components/schemas/ReviewIntent"
        category_id:
          type: string
          description: Opaque category id (cat_…) matched from the registry.
        category_confidence:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: >
            Agent-supplied confidence (0..1) in the category match (see
            SendRequest.category_confidence). Feeds the min_confidence auto-send
            gate only; the server never scores.
        composition_token:
          type: string
          description: Opaque token from a fresh, unfiltered GET /v1/rules for this agent, project, and category.
        idempotency_key:
          type: string
          deprecated: true
          description: >
            DEPRECATED body-level alias for the `Idempotency-Key` header (see
            SendRequest.idempotency_key). Send the header instead.

    ForwardRequest:
      type: object
      required: [to]
      properties:
        to:
          type: array
          items: { type: string, format: email }
          minItems: 1
        cc:
          type: array
          items: { type: string, format: email }
          description: Additional recipients on the forward. Screened by the same pre-flight as `to`.
        bcc:
          type: array
          items: { type: string, format: email }
          description: Blind recipients on the forward. Never rendered as a header.
        text: { type: string, description: Optional note prepended to the forwarded content. }
        html:
          type: string
          description: >
            Accepted and IGNORED. The forwarded content is a plain-text quote of the
            parent message; emitting an HTML alternative would show HTML-capable
            clients the note WITHOUT the forwarded thread.
        mode:
          type: string
          enum: [review, direct]
          description: >
            Review Loop per-send assertion (see SendRequest.mode). A forward is an
            outbound message to arbitrary NEW recipients that quotes an inbound
            thread, so it is governed by the same review policy as send.
        intent:
          $ref: "#/components/schemas/ReviewIntent"
        category_id:
          type: string
          description: Opaque category id (cat_…) matched from the registry.
        category_confidence:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: >
            Agent-supplied confidence (0..1) in the category match (see
            SendRequest.category_confidence).
        composition_token:
          type: string
          description: Opaque token from a fresh, unfiltered GET /v1/rules for this agent, project, and category.
        idempotency_key:
          type: string
          deprecated: true
          description: >
            DEPRECATED body-level alias for the `Idempotency-Key` header (see
            SendRequest.idempotency_key). Send the header instead.

    SendResult:
      type: object
      description: The outcome of a reply or forward.
      properties:
        message_id: { type: string }
        thread_id: { type: string }
        review_id:
          type: string
          description: >
            Opaque review id (rr_…) for the review row that governed this send.
            ADDITIVE. Every agent-plane send now creates one, so an agent that
            crashes after issuing the request can still call `GET /v1/reviews/{id}`
            and read `closed` / `sent_message_id` instead of guessing whether the
            message went out.

    SentResult:
      type: object
      description: >
        A Review Loop submit that was sent immediately (policy-permitted direct or
        graduated path). `kind` is always "sent".
      required: [kind, message]
      properties:
        kind: { type: string, enum: [sent] }
        message:
          type: object
          properties:
            id: { type: string }
            thread_id: { type: string }
        review:
          type: object
          description: The review row that governed this send (ADDITIVE).
          properties:
            id: { type: string, description: Opaque review id (rr_…). }
            state: { type: string, description: Terminal state (sent | auto_sent). }

    QueuedForReviewResult:
      type: object
      description: A Review Loop submit that was parked for human review.
      required: [kind, review]
      properties:
        kind: { type: string, enum: [queued_for_review] }
        review:
          type: object
          required: [id, state]
          properties:
            id: { type: string, description: Opaque review id (rr_…). }
            state: { type: string, description: Current review state (e.g. needs_review). }
            effective_mode:
              type: string
              enum: [review, direct]
              description: The mode after the policy resolved the agent's assertion.

    Review:
      type: object
      description: >
        A review request (rr_…); the server-owned pre-send record of a message
        under the Review Loop. Carries intent, the current proposed draft, category,
        state machine fields, and (once sent) the sent body + diff.
      required: [id, state, mode, effective_mode, kind, revision, version]
      properties:
        id: { type: string }
        state:
          type: string
          enum: [needs_review, in_review, chatting, stale, approved, sent, auto_sent, rejected, stalled, cancelled, failed]
        mode: { type: string, enum: [review, direct] }
        effective_mode: { type: string, enum: [review, direct] }
        kind: { type: string, enum: [send, reply, forward] }
        from_address: { type: string }
        agent_id: { type: string }
        category_id: { type: string }
        intent_summary: { type: string }
        intent_meta: { type: object, additionalProperties: true }
        revision: { type: integer }
        version: { type: integer }
        proposed_subject: { type: string }
        proposed_body_text: { type: string }
        proposed_body_html: { type: string }
        proposed_to: { type: array, items: { type: string } }
        proposed_cc: { type: array, items: { type: string } }
        proposed_bcc: { type: array, items: { type: string } }
        sent_subject: { type: string }
        sent_body_text: { type: string }
        diff_unified: { type: string }
        sent_message_id: { type: string }
        gate_outcome: { type: string }
        stale_reason: { type: string }
        decision_feedback: { type: string }
        closed:
          type: boolean
          description: >
            True once this review will never move again; `sent`, `auto_sent`,
            `cancelled` OR `failed`. It is the definitive per-review "am I done?"
            answer and the poll-side companion to the terminal review events; use it
            after a restart when your event cursor is gone. `failed` is included
            deliberately: nothing re-approves a failed review, so treating it as open
            means waiting forever.
        send_error:
          type: string
          description: Vendor-scrubbed delivery failure, present on a failed review.
        send_path:
          type: string
          enum: [human_reviewed, reviewer_approved, graduated_auto, agent_direct]
          description: How the message was released, once sent.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        decided_at: { type: string, format: date-time }
        sent_at: { type: string, format: date-time }

    ReviewPage:
      type: object
      required: [items, total]
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/Review" } }
        total: { type: integer }
        next_cursor: { type: string }

    ReviewTurn:
      type: object
      description: One immutable turn in a review's append-only thread (turn_…).
      required: [id, seq, turn_type, actor_kind, created_at]
      properties:
        id: { type: string }
        seq: { type: integer }
        turn_type:
          type: string
          enum: [agent_draft, agent_question, agent_note, human_comment, human_question, human_edit, human_approve, human_reject, human_reconfirm, system_diff, state_change, nudge]
        actor_kind: { type: string, enum: [agent, human, review_agent, system] }
        actor_id: { type: string }
        body: { type: string }
        revision: { type: integer }
        diff_json: { type: object, additionalProperties: true }
        metadata: { type: object, additionalProperties: true }
        created_at: { type: string, format: date-time }

    ReviewTurnPage:
      type: object
      required: [items, total]
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/ReviewTurn" } }
        total: { type: integer }

    PostReviewChatRequest:
      type: object
      description: >
        A chat turn on a review's thread (M5). The agent posts an agent_question
        turn; if the draft is in_review it flips to chatting; a feedback_added nudge
        is enqueued to the OTHER party (the human reviewer, via the journal/console
        stream) and a review.chat event is emitted. Idempotent on the Idempotency-Key.
      required: [text]
      properties:
        text: { type: string, description: "The agent's question/comment for the human reviewer." }

    SubmitRevisionRequest:
      type: object
      description: >
        Post a NEW agent draft under a parent_revision CAS (D17). parent_revision is
        the PRIMARY conflict check; it must equal the draft's current revision, else
        409 STALE with NO mutation (a human/inbound actor moved the draft). version is
        OPTIONAL belt-and-suspenders (a human edit that did not bump the revision still
        bumps the version). On a clean CAS the proposed_* body is re-rendered in place
        (revision++), the draft returns to needs_review, and the reviewer nudge +
        review event fire (all atomic). rules_version_seen pins the rule high-water the
        draft was composed against (born-stale basis, D19).
      required: [parent_revision]
      properties:
        parent_revision: { type: integer, description: "The revision the agent composed against (PRIMARY CAS)." }
        version: { type: integer, description: "Optional row-version CAS (defense in depth)." }
        subject: { type: string }
        text:
          type: string
          description: >
            Plain-text body of the redraft. This is the canonical name and matches
            SendRequest.text, ReplyRequest.text, ForwardRequest.text and
            Message.text, so the same field name composes a message and redrafts it.
        body:
          type: string
          deprecated: true
          description: >
            DEPRECATED alias for `text`, kept accepted indefinitely so
            already-deployed callers keep working; it will never be removed. Send
            `text` in new code. Supplying BOTH with different content is a 400
            (code bad_request, errors[].code conflicting_alias); the server never
            guesses which bytes the reviewer will be shown; supplying both with
            identical content is accepted. Note that Idempotency-Key replay hashes
            the RAW body, so the same redraft retried once as `body` and once as
            `text` under one key is a 409 idempotency_conflict.
        html: { type: string }
        built_at: { type: string, format: date-time, description: "When the agent built this draft (informational)." }
        rules_version_seen: { type: integer, description: "Rule high-water this draft was composed against (born-stale basis)." }
        composition_token:
          type: string
          description: Opaque token from a fresh, unfiltered GET /v1/rules for the review's category.
        attachments:
          type: array
          items: { $ref: "#/components/schemas/AttachmentInput" }
          description: >
            REPLACES the draft's attachments. Omit the field to leave them untouched;
            send an empty array to clear them. Without this a redraft could never
            restore an attachment, so an agent redrafting after feedback would ship a
            message the reviewer saw WITH a file and the recipient received without.

    RestampReviewRequest:
      type: object
      description: >
        The D19/§8 re-stamp-without-redraft escape valve ($0). The agent asserts it
        reviewed the draft against rules `against_version` and no change is needed; the
        server advances the draft's composed_* rules-versions WITHOUT a new draft. The
        optional house_style_version re-stamps the house-style axis too. against_version
        must not exceed the category's current rules-version (a re-stamp can never claim
        a version that does not exist yet).
      required: [against_version]
      properties:
        against_version: { type: integer, description: "The category rules-version the agent reviewed against (≤ the category's current rules-version)." }
        house_style_version: { type: integer, description: "Optional: re-stamp the house-style axis to this version (≤ the org's current house_style_version)." }

    ReviewFeedbackComment:
      type: object
      description: One human/agent comment in the assembled review feedback (spec §11).
      required: [turn_id, actor_kind, body, created_at]
      properties:
        turn_id: { type: string }
        actor_kind: { type: string, enum: [agent, human, review_agent, system] }
        actor_id: { type: string }
        body: { type: string }
        created_at: { type: string, format: date-time }

    ReviewFeedback:
      type: object
      description: >
        The human's assembled feedback for a review (spec §11): the unified + structured
        diff of the human's edit, the human comments/rejection feedback, the decision,
        and the rules born from this review (rule_ ids whose source_review_id is this
        review). Read-only; $0 LLM (pure assembly over stored turns/rules).
      required: [review_id, decision, comments, new_rules]
      properties:
        review_id: { type: string }
        decision: { type: string, description: "edited | approved | rejected | <live state>." }
        diff_unified: { type: string }
        diff_json: { type: object, additionalProperties: true }
        comments:
          type: array
          items: { $ref: "#/components/schemas/ReviewFeedbackComment" }
        new_rules:
          type: array
          items: { type: string }
          description: "rule_ ids whose provenance points at this review (M4 source_review_id)."

    ReviewEvent:
      type: object
      description: >
        One durable review nudge (ndg_…) as drained from the authoritative liveness
        queue (spec §11). seq is the per-review monotonic ordinal the ack cursor
        advances against (0 for a broadcast nudge). Opaque typed ids only (D10).
      required: [seq, id, reason, created_at]
      properties:
        seq: { type: integer }
        id: { type: string }
        reason:
          type: string
          description: >
            The work-item kind. sent / send_failed / cancelled are TERMINAL: the LAST
            event a review ever produces is one of these; ack it and stop polling that
            review. (Almost every review emits exactly one; a failed send the agent then
            closes out emits send_failed and then cancelled.) `sent` covers both delivery
            flavors; payload.state distinguishes sent from auto_sent. `approved` and
            `staleness` are RESERVED and never emitted (kept so the enum is never
            narrowed). Treat an unrecognized reason as ack-and-ignore: this enum is
            additive across 0.x releases.
          enum: [redraft_requested, feedback_added, recheck_category, staleness, approved, rejected, front_run_next, rule_changed, propagate_general_rule, sent, send_failed, cancelled]
        review_id: { type: string }
        category_id: { type: string }
        payload: { type: object, additionalProperties: true }
        created_at: { type: string, format: date-time }

    ReviewEventCursor:
      type: object
      description: The agent's per-(agent, review) ack frontier; its strict-FIFO position for one review.
      required: [review_id, last_acked_seq]
      properties:
        review_id: { type: string }
        last_acked_seq: { type: integer }

    ReviewEventsResponse:
      type: object
      description: The drain result for list_review_events / wait_for_review_event; un-acked events in FIFO seq order plus the per-review cursors.
      required: [events]
      properties:
        events: { type: array, items: { $ref: "#/components/schemas/ReviewEvent" } }
        cursors: { type: array, items: { $ref: "#/components/schemas/ReviewEventCursor" } }

    AckReviewEventRequest:
      type: object
      description: >
        Advance per-(agent, review) cursors and/or mark broadcast nudges done.
        Idempotent and monotonic; re-acking an older seq is a no-op (exactly-once
        effect, spec §6). Either set may be empty.
      properties:
        acks:
          type: array
          items:
            type: object
            required: [review_id, through_seq]
            properties:
              review_id: { type: string }
              through_seq: { type: integer }
        broadcast_ids: { type: array, items: { type: string } }

    AckReviewEventResponse:
      type: object
      required: [cursors]
      properties:
        cursors: { type: array, items: { $ref: "#/components/schemas/ReviewEventCursor" } }

    RealtimeTicket:
      type: object
      description: >
        A short-lived, single-use ticket the console creates (under its Clerk session)
        to open the live Approvals SSE stream; EventSource cannot set an
        Authorization header, so the customer scope is carried by this opaque ticket
        in the stream's `ticket` query parameter. Bound to the session's customer at
        create time, consumed on first stream connect, and self-expiring (~60s).
      required: [ticket, expires_at]
      properties:
        ticket: { type: string, description: Opaque single-use ticket (`rtk_…`). }
        expires_at: { type: string, format: date-time }

    AdminReviewListItem:
      type: object
      description: >
        One Approvals-queue row (console plane, D17). Metadata + intent summary +
        subject + a body snippet only; never the full body (loaded on detail open).
      required: [id, state, mode, effective_mode, kind, revision, version]
      properties:
        id: { type: string }
        state:
          type: string
          enum: [needs_review, in_review, chatting, stale, approved, sent, auto_sent, rejected, stalled, cancelled, failed]
        mode: { type: string, enum: [review, direct] }
        effective_mode: { type: string, enum: [review, direct] }
        kind: { type: string, enum: [send, reply, forward] }
        from_address: { type: string }
        agent_id: { type: string }
        category_id: { type: string }
        intent_summary: { type: string }
        subject: { type: string }
        snippet: { type: string }
        revision: { type: integer }
        version: { type: integer }
        stale_reason: { type: string }
        gate_outcome: { type: string, description: "The auto-send divert reason (held:<reason>) when the submit-time gate routed this message to the human (M6); empty for an ordinary review." }
        escalation_reason: { type: string, description: "Why a reviewer-loop draft landed back in the human queue (M8; §9): a reviewer's escalate, or a circuit breaker (max_hops / review_deadline) forcing it to the human. Empty for an ordinary review." }
        hop_count: { type: integer, description: "The monotonic reviewer hand-back counter (M8 breaker (a)); surfaced so a maxed-out review reads as forced-to-human." }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    AdminReviewPage:
      type: object
      required: [items, total]
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/AdminReviewListItem" } }
        total: { type: integer }
        next_cursor: { type: string }

    AdminReviewDetail:
      type: object
      description: >
        Full console review detail (console plane): the review (intent + current
        draft + diff if revised) plus the append-only thread turns.
      required: [review, turns]
      properties:
        review: { $ref: "#/components/schemas/Review" }
        turns: { type: array, items: { $ref: "#/components/schemas/ReviewTurn" } }

    AdminReviewAction:
      type: object
      description: >
        A human-authority review action body (console plane, D17). revision +
        version are the CAS naming the draft the human acted on (a mismatch is a
        409 STALE). subject/body are supplied only on edit-send; feedback is the
        optional rule-birth signal (D7, empty allowed).
      required: [revision, version]
      properties:
        revision: { type: integer }
        version: { type: integer }
        subject: { type: string, description: "Edited subject (edit-send only)." }
        body: { type: string, description: "Edited body text (edit-send only)." }
        feedback: { type: string, description: "Optional decision comment / rule-birth signal." }

    AdminReviewChatRequest:
      type: object
      description: >
        The console chat side-panel body (M5; spec §10): a human comment whose OTHER
        party is the composing agent. CONSOLE-ONLY (never an MCP tool, D17).
      required: [text]
      properties:
        text: { type: string, description: "The human's comment for the composing agent." }

    AdminReviewSendResult:
      type: object
      description: >
        The approve / edit-send success result. The platform performed the authenticated send
        via the COMPOSER's inbox; the send is irreversible once returned.
      required: [status, review_id, state, message_id]
      properties:
        status: { type: string, example: sent }
        review_id: { type: string }
        state: { type: string }
        message_id: { type: string }
        thread_id: { type: string }

    AdminReviewRejectResult:
      type: object
      required: [status, review_id, state]
      properties:
        status: { type: string, example: rejected }
        review_id: { type: string }
        state: { type: string }

    Category:
      type: object
      description: >
        A category (cat_…) in the registry (Review Loop, D9/D10). name + description
        are the skill-style metadata an agent fuzzy-matches against (NO LLM on our
        side; the agent does the matching). Categories are CUSTOMER-scoped and
        agent-attributed; nothing keys on the name (renames never break a reference).
      required: [id, name, description, scope, state, author_kind, rule_high_water, rules_version]
      properties:
        id: { type: string, description: Opaque category id (cat_…). }
        name: { type: string }
        description: { type: string }
        scope: { type: string, enum: [org_shared, agent_private] }
        state: { type: string, enum: [supervised, auto_notify, auto_silent] }
        merged_into: { type: string, description: Survivor category id when this one was merged/soft-deleted (cat_…). }
        created_by_agent_id: { type: string }
        author_kind: { type: string, enum: [agent, human] }
        rule_high_water: { type: integer }
        rules_version: { type: integer }
        never_graduate: { type: boolean, description: "Human lock pinning the category at supervised (no live auto-send; D16)." }
        clean_approval_count: { type: integer, description: "Clean human-approve-as-is count toward the maturity gate (D16)." }
        drift_count: { type: integer, description: "Human edit/reject count in a graduated category toward drift auto-demote (K; §7)." }
        graduated_at: { type: string, format: date-time, description: "When the category was last graduated (omitted if never)." }
        graduated_by: { type: string, description: "Who last graduated it ('human:<sub>'); empty if never/demoted." }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    CategoryPage:
      type: object
      required: [items, total]
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/Category" } }
        total: { type: integer }

    ProposeCategoryRequest:
      type: object
      description: >
        Propose a new category on the fly (D9). It stands immediately (author_kind=
        agent, state supervised) and writes a create audit/undo row.
      required: [name]
      properties:
        name: { type: string, description: Display name (mutable metadata; never a reference key). }
        description: { type: string, description: Skill-style matcher text the agent reads (mutable). }
        scope: { type: string, enum: [org_shared, agent_private], description: Defaults to org_shared. }

    AdminCreateCategoryRequest:
      type: object
      description: Create a human-authored shared category from the console.
      required: [name]
      properties:
        name: { type: string, description: Display name (mutable metadata; never a reference key). }
        description: { type: string, description: Skill-style matcher text the agent reads (mutable). }
        scope: { type: string, enum: [org_shared, agent_private], description: Defaults to org_shared. }

    UpdateCategoryRequest:
      type: object
      description: Rename / re-describe a category; metadata ONLY (D10). Either field may be omitted (unchanged).
      properties:
        name: { type: string }
        description: { type: string }

    MergeCategoryRequest:
      type: object
      description: >
        Merge the path category (the loser) into into_category_id (the survivor),
        repointing all rr_/rule_ references by cat_ id in one transaction (D9/D10). A
        cross-tenant or unknown into_category_id is 404 (never a cross-customer merge).
      required: [into_category_id]
      properties:
        into_category_id: { type: string, description: The survivor category id (cat_…). }

    MergeCategoryResult:
      type: object
      description: The survivor category plus how many references were repointed by the merge.
      required: [category, review_requests_repointed, writing_rules_repointed]
      properties:
        category: { $ref: "#/components/schemas/Category" }
        review_requests_repointed: { type: integer }
        writing_rules_repointed: { type: integer }

    DeleteCategoryResult:
      type: object
      description: The result of a soft-delete (the row + history survive as training data; D9).
      required: [status, category_id]
      properties:
        status: { type: string, example: deleted }
        category_id: { type: string }
        merged_into: { type: string }

    AccountRiskDial:
      type: object
      description: >
        The account-wide default risk dial (org_settings; D4/D12); the values a
        per-category null override inherits. The single user-configurable brand-risk
        lever (canary rate, drift-demote K, maturity-gate thresholds, min confidence,
        first-contact gate, per-day auto-send cap).
      required: [min_confidence, first_contact_gate, drift_demote_after, canary_rate, graduate_min_approvals, graduate_min_age_hours, auto_send_cap_per_day]
      properties:
        min_confidence: { type: number, format: double }
        first_contact_gate: { type: boolean }
        drift_demote_after: { type: integer, description: "Drift-demote threshold K." }
        canary_rate: { type: number, format: double }
        graduate_min_approvals: { type: integer, description: "Maturity-gate N (clean approvals)." }
        graduate_min_age_hours: { type: integer, description: "Maturity-gate minimum category age." }
        auto_send_cap_per_day: { type: integer, description: "Plan-tier per-customer auto-send volume cap (D1)." }

    EffectiveRiskDial:
      type: object
      description: >
        The RESOLVED risk dial for a category: the account default with any per-
        category override applied (null override = inherit; D12). Every field is a
        concrete value already in force.
      required: [min_confidence, first_contact_gate, drift_demote_after, canary_rate, graduate_min_approvals, graduate_min_age_hours, auto_send_cap_per_day]
      properties:
        min_confidence: { type: number, format: double }
        first_contact_gate: { type: boolean }
        drift_demote_after: { type: integer }
        canary_rate: { type: number, format: double }
        graduate_min_approvals: { type: integer }
        graduate_min_age_hours: { type: integer }
        auto_send_cap_per_day: { type: integer }

    CategoryRiskDial:
      type: object
      description: >
        One category's risk-dial OVERRIDE columns (null = inherit the account default;
        D12) alongside the resolved effective dial. A null field means the category
        inherits that value from the account default.
      required: [category_id, effective]
      properties:
        category_id: { type: string }
        min_confidence: { type: number, format: double, nullable: true }
        first_contact_gate: { type: boolean, nullable: true }
        drift_demote_after: { type: integer, nullable: true }
        canary_rate: { type: number, format: double, nullable: true }
        graduate_min_approvals: { type: integer, nullable: true }
        graduate_min_age_hours: { type: integer, nullable: true }
        effective: { $ref: "#/components/schemas/EffectiveRiskDial" }

    RiskDial:
      type: object
      description: >
        The effective risk dial (Review Loop, agent plane; D4/D12): the account default
        plus every category's overrides (each with its resolved effective value).
        Read-only for agents; flipping the dial is a console (human) action (D16).
      required: [account, categories]
      properties:
        account: { $ref: "#/components/schemas/AccountRiskDial" }
        categories: { type: array, items: { $ref: "#/components/schemas/CategoryRiskDial" } }

    RiskDialRequest:
      type: object
      description: >
        Set the account-default risk dial (console plane, D4). A nil field leaves that
        column unchanged.
      properties:
        min_confidence: { type: number, format: double }
        first_contact_gate: { type: boolean }
        drift_demote_after: { type: integer }
        canary_rate: { type: number, format: double }
        graduate_min_approvals: { type: integer }
        graduate_min_age_hours: { type: integer }
        auto_send_cap_per_day: { type: integer }

    CategoryRiskDialRequest:
      type: object
      description: >
        Set / clear a category's risk-dial overrides (console plane, D12). For each
        field, a non-null value SETS the override; an explicit JSON null RESETS it to
        inherit; an omitted field is unchanged.
      properties:
        min_confidence: { type: number, format: double, nullable: true }
        first_contact_gate: { type: boolean, nullable: true }
        drift_demote_after: { type: integer, nullable: true }
        canary_rate: { type: number, format: double, nullable: true }
        graduate_min_approvals: { type: integer, nullable: true }
        graduate_min_age_hours: { type: integer, nullable: true }

    GraduationStatus:
      type: object
      description: >
        The graduation gate status toward the NEXT rung (Review Loop, D16). Reports
        clean approvals (N / needed), category age, the maturity gate (auto_silent
        precondition), the drift counter vs K, and whether a human graduate would
        succeed right now (can_graduate). Read-only; agents propose but never flip the
        bit.
      required:
        - category_id
        - state
        - next_state
        - never_graduate
        - clean_approval_count
        - graduate_min_approvals
        - approvals_met
        - age_hours
        - graduate_min_age_hours
        - age_met
        - maturity_gate_met
        - drift_count
        - drift_demote_after
        - can_graduate
      properties:
        category_id: { type: string }
        state: { type: string, enum: [supervised, auto_notify, auto_silent] }
        next_state: { type: string, description: "The rung a graduate would move to (empty if none)." }
        never_graduate: { type: boolean, description: "Pinned at supervised (high-stakes lock)." }
        clean_approval_count: { type: integer }
        graduate_min_approvals: { type: integer }
        approvals_met: { type: boolean }
        age_hours: { type: number, format: double }
        graduate_min_age_hours: { type: integer }
        age_met: { type: boolean }
        maturity_gate_met: { type: boolean }
        drift_count: { type: integer }
        drift_demote_after: { type: integer }
        can_graduate: { type: boolean }

    ScanBacklogStatus:
      type: object
      description: >
        The D19/§8 backlog-reconciliation snapshot for a category (agent-readable,
        $0-LLM). Counts the QUEUED drafts that are stale vs current-enough against the
        current category rules-version + house-style version (a pure integer compare).
        Read-only; the agent READS the picture; the human / hooks TRIGGER the sweep.
      required:
        - category_id
        - state
        - queued
        - current_enough
        - stale
        - current_category_rules_version
        - current_house_style_version
        - staleness_tolerance
      properties:
        category_id: { type: string }
        state: { type: string, enum: [supervised, auto_notify, auto_silent, probation] }
        queued: { type: integer, description: "How many drafts are in the human queue (needs_review|in_review|chatting)." }
        current_enough: { type: integer, description: "Of the queued, how many are within the staleness tolerance of the current rules-version." }
        stale: { type: integer, description: "Of the queued, how many were composed under older rules and need a redraft." }
        current_category_rules_version: { type: integer }
        current_house_style_version: { type: integer }
        staleness_tolerance: { type: integer, description: "How many versions behind current a draft may be and still count current-enough." }

    ScanBacklogRequest:
      type: object
      description: >
        The D19/§8 on-demand reconciliation sweep request (console). staleness_tolerance
        overrides the strict default (0 = must be at/above current); null = the default.
      properties:
        staleness_tolerance: { type: integer, description: "How many rules-versions behind current a queued draft may be and still release." }

    PacingItem:
      type: object
      description: >
        One queued draft's pacing classification relative to the human review cursor +
        lookahead window (M7 Slice B/§8). $0-LLM (a cheap integer staleness compare).
      required:
        - review_id
        - state
      properties:
        review_id: { type: string }
        state:
          type: string
          enum: [behind_cursor, in_window_fresh, in_window_redrafting, ahead]
          description: >
            behind_cursor = the human has reviewed past it; in_window_fresh = in the
            freshness-guaranteed window and current-enough; in_window_redrafting = in
            the window but stale (a redraft is in flight; the console shows a shimmer);
            ahead = queued beyond the window, not yet freshness-guaranteed.

    CategoryPacingState:
      type: object
      description: >
        The demand-driven pacing snapshot for a category (agent-readable, $0-LLM,
        M7 Slice B/§8): the human review cursor position, the effective window/ceiling/
        interval, the queued count, and each queued draft's in-window/redrafting/
        behind-cursor classification. Read-only; the cursor advances from the human's
        console approve/reject/edit actions.
      required:
        - category_id
        - cursor_advanced_count
        - lookahead_window
        - rework_batch_max
        - nudge_min_interval_ms
        - queued
        - in_window
        - redrafting
        - items
      properties:
        category_id: { type: string }
        cursor_review_id: { type: string, description: "The last queued draft the human acted on (the cursor); omitted when nothing reviewed yet." }
        cursor_advanced_count: { type: integer, description: "Monotonic count of cursor advances." }
        lookahead_window: { type: integer, description: "Effective freshness window (default org_settings.lookahead_window=3)." }
        rework_batch_max: { type: integer, description: "HARD per-nudge fan-out ceiling (default org_settings.rework_batch_max=10); one nudge can never fan to 500." }
        nudge_min_interval_ms: { type: integer, description: "Per-agent token-bucket interval that coalesces feedback storms (default 5000)." }
        queued: { type: integer, description: "How many drafts are in the human queue (needs_review|in_review|chatting)." }
        in_window: { type: integer, description: "Of the queued, how many sit in the freshness-guaranteed window." }
        redrafting: { type: integer, description: "Of the in-window, how many are stale and being redrafted (the console shimmer set)." }
        items:
          type: array
          items: { $ref: "#/components/schemas/PacingItem" }

    ReconcileMessageOutcome:
      type: object
      description: >
        One per-message reconciliation outcome (D19); WHY a queued draft released or
        was held. verdict is the integer-compare result; released is true only when a
        current-enough draft also passed the M6 gate; a stale draft is NEVER released
        as-is.
      required: [review_id, verdict, released, composed_category_rules_version, composed_house_style_version, current_category_rules_version, current_house_style_version, nudged]
      properties:
        review_id: { type: string }
        verdict: { type: string, enum: [current_enough, stale] }
        released: { type: boolean }
        gate_outcome: { type: string, description: "The M6 gate decision for a current-enough draft (auto_silent|auto_notify, or held:<reason>)." }
        held_reason: { type: string, description: "The held token when a gate diverted a current-enough draft." }
        composed_category_rules_version: { type: integer }
        composed_house_style_version: { type: integer }
        current_category_rules_version: { type: integer }
        current_house_style_version: { type: integer }
        nudged: { type: boolean, description: "Whether a reprocess nudge was enqueued (redraft a stale draft, or re-submit a releasable one)." }

    ReconcileResult:
      type: object
      description: >
        The D19/§8 reconciliation sweep summary (console scan-backlog / graduate /
        rule-change). Records every per-message decision so the human never hand-touches
        the backlog and no stale draft slips out. Deterministic, $0-LLM.
      required: [category_id, trigger, scanned, released, stale, held_by_gate, current_category_rules_version, current_house_style_version, staleness_tolerance, outcomes]
      properties:
        category_id: { type: string }
        trigger: { type: string, enum: [graduate, rule_change, scan_backlog], description: "What kicked off the sweep." }
        scanned: { type: integer }
        released: { type: integer, description: "Current-enough drafts released through the M6 gate (re-submit)." }
        stale: { type: integer, description: "Stale drafts nudged to redraft against current rules (held, not released)." }
        held_by_gate: { type: integer, description: "Current-enough drafts that a M6 gate diverted (transient re-supervision)." }
        current_category_rules_version: { type: integer }
        current_house_style_version: { type: integer }
        staleness_tolerance: { type: integer }
        outcomes:
          type: array
          items: { $ref: "#/components/schemas/ReconcileMessageOutcome" }

    ProposeGraduationRequest:
      type: object
      description: >
        Record an agent's request to graduate a category (D16/D6). evidence is an
        opaque agent-supplied object stored on the request transition for the human to
        review. It does NOT flip the bit.
      properties:
        evidence: { type: object, additionalProperties: true }

    GraduateCategoryRequest:
      type: object
      description: Graduate a category ONE rung up to to_state (console plane, D16).
      required: [to_state]
      properties:
        to_state: { type: string, enum: [auto_notify, auto_silent] }

    NeverGraduateRequest:
      type: object
      description: Set/clear the never-graduate lock on a category (console plane, D16).
      required: [locked]
      properties:
        locked: { type: boolean, description: "true pins at supervised (demoting first if graduated); false releases the pin." }

    ReviewPolicy:
      type: object
      description: >
        The account-default review policy (D6) plus the effective floor an inbox with
        no override resolves to (require_review is the safe floor).
      required: [policy, effective]
      properties:
        policy: { type: string, enum: [require_review, allow_direct, auto_send_graduated] }
        effective: { type: string, enum: [require_review, allow_direct, auto_send_graduated] }

    ReviewPolicyRequest:
      type: object
      description: Set the account-default review policy (D6). An invalid value is 400.
      required: [policy]
      properties:
        policy: { type: string, enum: [require_review, allow_direct, auto_send_graduated] }

    InboxReviewPolicy:
      type: object
      description: >
        The per-inbox review-policy override view after a set/clear: the raw override
        (empty = inherit), the account default it inherits from, and the resolved
        effective policy for this inbox (override-or-account-or-floor; D6).
      required: [address, override, account, effective]
      properties:
        address: { type: string }
        override: { type: string, description: "Per-inbox override; empty = inherit the account default." }
        account: { type: string, enum: [require_review, allow_direct, auto_send_graduated] }
        effective: { type: string, enum: [require_review, allow_direct, auto_send_graduated] }

    InboxReviewPolicyRequest:
      type: object
      description: >
        Set the per-inbox override (a non-null policy) or clear it back to inherit the
        account default (explicit JSON null; D6).
      properties:
        policy:
          type: string
          nullable: true
          enum: [require_review, allow_direct, auto_send_graduated]
          description: "A value SETS the override; explicit null CLEARS it (= inherit)."

    Rule:
      type: object
      description: >
        A learned writing rule (rule_…) in the Review Loop (D2/D11). House-style/
        general (scope='general', applies across all categories) or category-scoped.
        Append-only by supersession: an edit is a new rev (same lineage_id) with the
        prior flipped to superseded. Read by the agent at compose/redraft time via the
        ordered get_rules ladder; we never apply it. Opaque ids only (D10).
      required: [id, lineage_id, rev, scope, rule_text, kind, priority, status, author_kind, created_at, updated_at]
      properties:
        id: { type: string, description: Opaque rule id (rule_…). }
        rule_layer:
          type: string
          enum: [org, project]
          description: >
            Ownership layer (org/project model). `org` = house-style inherited by
            every project in the org; `project` = layered on top (the agent-plane
            default). Project/per-agent rules outrank broader org rules in the
            ordered get_rules ladder.
        org_id: { type: string, description: The org this rule belongs to. }
        project_id: { type: string, description: The project this rule belongs to; empty for an org-layer rule. }
        lineage_id: { type: string, description: Stable lineage handle across versions (rln_…). }
        rev: { type: integer }
        scope: { type: string, enum: [general, category], description: general = house-style (D2). }
        category_id: { type: string, description: Set iff scope=category (cat_…). }
        scope_agent_id: { type: string, description: Set for a per-agent override; empty = all org agents. }
        rule_text: { type: string }
        kind: { type: string, enum: [soft, hard], description: hard = non-overridable. }
        priority: { type: integer }
        status: { type: string, enum: [proposed, active, superseded, retired] }
        supersedes_id: { type: string, description: The prior rev this row superseded (rule_…). }
        author_kind: { type: string, enum: [agent, human] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    RulePage:
      type: object
      description: >
        The ORDERED active rule set (the §7 precedence ladder applied SERVER-SIDE,
        capped). Category rules precede general/house-style rules; within each, highest
        precedence first. The agent reconciles the list semantically (NO LLM on our side).
      required: [items, total]
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/Rule" } }
        total: { type: integer }
        house_style_version: { type: integer }
        category_rules_version: { type: integer }
        rule_high_water: { type: integer }
        composition_token:
          type: string
          description: Opaque short-lived proof of this exact effective rules snapshot. Omitted on filtered scope queries.
        composition_token_expires_at: { type: string, format: date-time }

    Suppression:
      type: object
      description: >
        One recipient opt-out (suppression) row visible to the caller's own org. Only
        org-scope rows are ever returned to the agent/console planes; global and
        shared_domain rows are operator-only. Rows soft-revoke (revoked_at/by/reason)
        rather than delete, so opt-out history outlives tenant lifecycle.
      required: [id, recipient, scope, source, reactivation_count, created_at, revoked]
      properties:
        id: { type: string, description: "Opaque suppression id (sup_…)." }
        recipient: { type: string, description: "Canonicalized recipient address (NFC, punycode domain, lower-cased)." }
        recipient_raw: { type: string, description: "The address as originally received." }
        scope: { type: string, enum: [org], description: "Always 'org' on these planes (global/shared_domain are operator-only)." }
        source: { type: string, enum: [one_click, page, mailto, reply_stop, manual, complaint, escalation] }
        narrow_agent_id: { type: string, description: "Set on a recipient-chosen 'just this sender' narrow row." }
        narrow_mailbox: { type: string }
        origin_mailbox: { type: string, description: "The sending mailbox that prompted the opt-out (audit)." }
        origin_agent_id: { type: string }
        origin_message_id: { type: string }
        reactivation_count: { type: integer, description: "Cumulative unsub→revoke→unsub cycles for this row (the §7 abuse input)." }
        created_at: { type: string, format: date-time }
        revoked_at: { type: string, format: date-time, description: "Set when the row was soft-revoked." }
        revoked_by: { type: string }
        revoke_reason: { type: string }
        revoked: { type: boolean }

    SuppressionPage:
      type: object
      description: The paged list of the caller's own org suppression rows (newest-first).
      required: [items, total]
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/Suppression" } }
        total: { type: integer }
        next_cursor: { type: string, description: "Opaque cursor for the next page; absent on the last page." }

    ReputationMetrics:
      type: object
      description: The latest window's Sends/Bounces/Complaints rollup for the org.
      required: [sends, bounces, complaints, bounce_rate, complaint_rate]
      properties:
        window_start: { type: string, format: date-time }
        window_end: { type: string, format: date-time }
        sends: { type: integer }
        bounces: { type: integer }
        complaints: { type: integer }
        bounce_rate: { type: number }
        complaint_rate: { type: number }

    ReputationRollup:
      type: object
      description: >
        The org's provider-neutral deliverability rollup: derived status badge,
        sending readiness, the latest metrics window, and open-finding count.
      required: [object, org_id, status, sending_status, configured, metrics, open_findings]
      properties:
        object: { type: string, enum: [reputation] }
        org_id: { type: string }
        status: { type: string, description: "UI badge: healthy/at_risk/paused/enforced/unknown." }
        sending_status: { type: string, description: "The org's effective sending status." }
        configured: { type: boolean, description: "Whether outbound sending infrastructure is configured." }
        last_checked_at: { type: string, format: date-time }
        metrics: { $ref: "#/components/schemas/ReputationMetrics" }
        open_findings: { type: integer, description: "Count of open findings for the org." }

    ReputationFinding:
      type: object
      description: One deliverability finding (internal status/severity/type; raw provider vocabulary is not exposed).
      required: [id, type, severity, status, title, detail, first_seen_at, last_seen_at]
      properties:
        id: { type: string }
        type: { type: string, description: "bounce/complaint/feedback_3p/ip_listing/dkim/dmarc/spf/bimi/suppression/account_health/unknown." }
        severity: { type: string, enum: [low, high, unknown] }
        status: { type: string, enum: [open, resolved] }
        domain: { type: string }
        sender: { type: string }
        title: { type: string }
        detail: { type: string }
        first_seen_at: { type: string, format: date-time }
        last_seen_at: { type: string, format: date-time }
        resolved_at: { type: string, format: date-time }

    ReputationFindingsPage:
      type: object
      description: The paged list of the org's deliverability findings (newest-first).
      required: [items, total]
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/ReputationFinding" } }
        total: { type: integer }
        next_cursor: { type: string, description: "Opaque cursor for the next page; absent on the last page." }

    OutboundRouteView:
      type: object
      description: >
        A provider-neutral view of a domain's current outbound route. An
        unassigned domain has assigned=false and route_state "unassigned" (it
        routes direct by fail-safe).
      required: [object, domain, assigned, status]
      properties:
        object: { type: string, enum: [outbound_route] }
        domain: { type: string }
        assigned: { type: boolean, description: "True when a route row exists for the domain." }
        route_state: { type: string, enum: [unassigned, provisioning, active, switching, deprovisioning, failed, paused] }
        status: { type: string, enum: [pending, setting_up, ready, needs_attention] }
        switched_at: { type: string, format: date-time }

    OutboundRouteRetirement:
      type: object
      description: >
        Provider-neutral progress for one old sending route being cleaned up after
        a switch. Infrastructure identifiers and snapshots are never exposed.
      required: [object, status, decommission_due_at, created_at]
      properties:
        object: { type: string, enum: [outbound_route_retirement] }
        status: { type: string, enum: [cleanup_scheduled] }
        decommission_due_at: { type: string, format: date-time, description: "When the retiring stint's records may be pruned." }
        created_at: { type: string, format: date-time }

    OutboundRouteRetirementList:
      type: object
      description: The domain's retiring-sender ledger rows (list envelope).
      required: [object, data]
      properties:
        object: { type: string, enum: [list] }
        data:
          type: array
          items: { $ref: "#/components/schemas/OutboundRouteRetirement" }
        has_more: { type: boolean }
        next_cursor: { type: string, nullable: true }

    SuppressionPrecheck:
      type: object
      description: >
        GET /v1/suppressions?recipient= pre-check result: whether the CALLER'S OWN ORG
        has an active suppression for the recipient, plus those org rows. Reflects only
        the caller's org state; never a global/shared/cross-tenant opt-out (R4
        non-leakage).
      required: [recipient, suppressed, rows]
      properties:
        recipient: { type: string, description: "The canonicalized recipient that was checked." }
        suppressed: { type: boolean, description: "True iff the caller's org has an active suppression for the recipient." }
        rows: { type: array, items: { $ref: "#/components/schemas/Suppression" } }

    RevokeSuppressionRequest:
      type: object
      description: >
        Revoke (un-suppress) one org-scope row. The reason is REQUIRED and audit-logged.
      required: [reason]
      properties:
        reason: { type: string, description: "Why the suppression is being revoked (required, audit-logged)." }

    SaveRuleRequest:
      type: object
      description: >
        Write or edit a rule (append-only by supersession; D11). scope='general' iff
        category_id is empty (house-style, D2). With supersedes_id the write is an EDIT
        (rev+1, same lineage). kind defaults soft. propagate_to_pending is the D8 retro-
        propagation HUMAN OPT-IN (default OFF): when true, a NEW category rule that could
        apply to pending siblings enqueues ONE propagate_general_rule nudge (siblings +
        suggested_batch) so the agent redrafts a FEW at a time; never the whole queue.
        Agent-plane saves are ALWAYS project-layer (the saved rule's rule_layer is
        `project`, bound to the key's project); creating org-layer/house-style rules is
        console/admin-only in v1.
      required: [rule_text]
      properties:
        client_id: { type: string, description: "Stable retry identifier; also accepted through Idempotency-Key. Replays the original response for 24 hours." }
        scope: { type: string, enum: [general, category], description: "Defaults from category_id (general iff empty)." }
        category_id: { type: string, description: "Category id (cat_…); empty = house-style/general." }
        rule_text: { type: string }
        kind: { type: string, enum: [soft, hard], description: "Defaults soft." }
        priority: { type: integer }
        source_review_id: { type: string, description: Provenance (rr_…). }
        source_turn_id: { type: string, description: Provenance (turn_…). }
        supersedes_id: { type: string, description: "Set to EDIT the prior version (rule_…)." }
        scope_agent_id: { type: string, description: "Set for a per-agent override; empty = all org agents." }
        propagate_to_pending: { type: boolean, description: "D8 retro-propagation HUMAN OPT-IN (default false): enqueue a propagate_general_rule nudge to pending siblings of a NEW category rule." }
        suggested_batch: { type: integer, description: "Override the propagate batch (0 = base 3, bounded by rework_batch_max); never fans one nudge to the whole queue." }

    PromoteRuleRequest:
      type: object
      description: Move a rule between the category and general/house-style layers (via a supersession).
      required: [to_scope]
      properties:
        to_scope: { type: string, enum: [general, category] }

    RuleAuditEntry:
      type: object
      description: >
        One append-only rule/category change & undo audit row (udo_…). before_json /
        after_json carry the snapshot enabling an undo. undo = a later 'restore' entry;
        this row's undone flips true when it has been undone.
      required: [id, entity_kind, entity_id, action, actor_kind, undone, created_at]
      properties:
        id: { type: string, description: Opaque audit id (udo_…). }
        entity_kind: { type: string, enum: [rule, category] }
        entity_id: { type: string, description: rule_… (lineage head) or cat_…. }
        action: { type: string, enum: [create, supersede, retire, rename, redescribe, merge, restore] }
        actor_kind: { type: string, enum: [agent, human, system] }
        actor_id: { type: string }
        before_json: { type: string, description: JSON snapshot of the prior version. }
        after_json: { type: string, description: JSON snapshot of the new version. }
        undone: { type: boolean }
        created_at: { type: string, format: date-time }

    RuleAuditPage:
      type: object
      required: [items, total]
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/RuleAuditEntry" } }
        total: { type: integer }
        next_cursor: { type: string }

    MarkMessageRequest:
      type: object
      properties:
        read:
          type: boolean
          description: Set the native IMAP \Seen flag (true = read, false = unread).

    BatchUpdateMessagesRequest:
      type: object
      description: >
        Apply a read-state toggle and/or a folder move to a list of message ids
        that all belong to the path inbox. At least one of `read` / `folder` must
        be set. Ids that are malformed or address a different inbox are reported
        in the response's `failed` list rather than failing the whole batch.
      required: [ids]
      properties:
        ids:
          type: array
          items: { type: string }
          description: Opaque message ids (msg_…), all owned by the path inbox. Max 200.
        read:
          type: boolean
          description: When present, set (true) or clear (false) the \Seen flag on each id.
        folder:
          type: string
          description: >
            When present, move each message to this folder. One of INBOX, Sent,
            Trash, Junk, Archive.
          enum: [INBOX, Sent, Trash, Junk, Archive]

    BatchUpdateResult:
      type: object
      description: Per-id outcome of a batch update.
      required: [updated, failed]
      properties:
        updated:
          type: array
          items: { type: string }
          description: Ids that were updated.
        failed:
          type: array
          items: { type: string }
          description: Ids skipped (not found, malformed, or not owned by this inbox).

    DeleteResult:
      type: object
      description: Outcome of a message or thread delete.
      required: [id, deleted, expunged, count]
      properties:
        id:
          type: string
          description: The message id or thread id that was deleted.
        deleted:
          type: boolean
          description: Always true on success.
        expunged:
          type: boolean
          description: >
            true when the message(s) were permanently removed; false when moved
            to Trash (a recoverable soft delete).
        count:
          type: integer
          description: Number of messages affected (1 for a message; the thread size for a thread).

    WaitRequest:
      type: object
      description: >
        Block until a matching message arrives. The inbox is taken from the path on
        `POST /v1/inboxes/{inbox_id}/wait`; on the legacy `POST /v1/wait` it is the
        `address` field.
      properties:
        address: { type: string, description: Required on POST /v1/wait; ignored on the inbox-scoped path. }
        from: { type: string, description: Substring match on the From header. }
        subject: { type: string, description: Substring match on the Subject header. }
        match: { type: string, description: Regex matched over subject + body. }
        folder: { type: string }
        link_hint: { type: string, description: Hint to bias verification-link extraction. }
        timeout_seconds:
          type: integer
          description: Max seconds to block (default 300, capped at 600).
        since_now:
          type: boolean
          description: When true (default), match only arrivals after the wait begins.

    ExtractedCredentials:
      type: object
      description: OTP code / verification link extracted from the matched message.
      properties:
        otp: { type: string, nullable: true }
        link: { type: string, nullable: true }

    WaitResult:
      type: object
      properties:
        timed_out: { type: boolean }
        message:
          allOf: [{ $ref: "#/components/schemas/Message" }]
          nullable: true
        extracted: { $ref: "#/components/schemas/ExtractedCredentials" }

    WhoAmI:
      type: object
      description: >
        The verified principal behind an agent key. org_id/project_id are the FIXED
        org/project the key is bound to (resolved from the stored key, never client
        input). There is NO mutable project selector for a scoped key; whoami is the
        canonical project-visibility surface; project selection happens when the
        human/admin issues the enrollment token or agent key.
      properties:
        customer_id: { type: string }
        org_id: { type: string, description: The fixed org the key is bound to. }
        project_id: { type: string, description: The fixed project the key is bound to. }
        agent_id: { type: string }
        key_id: { type: string }
        scopes:
          type: array
          items: { type: string }

    EnrollRequest:
      type: object
      required: [token]
      properties:
        token: { type: string, example: pk_enroll_<id>_<secret> }
        agent_handle: { type: string, description: Idempotency key for the agent identity. }
        agent_name: { type: string }
        client_id:
          type: string
          description: >
            Optional idempotency key (alias for the `Idempotency-Key` header). A
            retry with the same key replays the original enrollment response.

    EnrollResult:
      type: object
      properties:
        agent_id: { type: string }
        agent_key: { type: string, description: "pk_agent_..., shown once." }
        scopes:
          type: array
          items: { type: string }
        org_id: { type: string, description: The fixed org the new key is bound to (the token's resolved org). }
        project_id: { type: string, description: The fixed project the new key is bound to (the token's resolved project); the agent cannot change it. }

    SignUpRequest:
      type: object
      required: [human_email]
      properties:
        human_email: { type: string, format: email, description: Where the verification code is sent. }
        username:
          type: string
          description: >
            Desired local part for the first free inbox. It is normalized to lowercase,
            must contain at least five characters, and cannot be postmaster, admin,
            webadmin, legal, fraudmark, fraudmarc, keith, melissa, richard, sydney,
            syd, john, or johnny. Omit it for a generated local part.

    SignUpResult:
      type: object
      description: >
        The verification-only `signup:verify` bootstrap key (shown once) plus the
        pending inbox on `free.extrovertmail.com`. The account is not active until OTP
        verification. Before then the key may call signup verification only; it cannot
        create or read inboxes, export credentials, or send customer-authored mail. The
        key expires at `otp_expires_at`; the OTP is sent only to `human_email`.
      properties:
        customer_id: { type: string }
        agent_id: { type: string }
        agent_key: { type: string, description: Limited-scope bootstrap key, shown once and expiring with the OTP. }
        key_prefix: { type: string }
        scopes:
          type: array
          items: { type: string }
        address: { type: string, format: email, description: The pending inbox on `free.extrovertmail.com`. }
        verified: { type: boolean }
        otp_sent_to: { type: string }
        otp_expires_at: { type: string, format: date-time }
        message: { type: string }

    VerifyRequest:
      type: object
      required: [otp]
      properties:
        otp: { type: string }

    MailboxQuickstartCall:
      type: object
      description: A copy-ready MCP tool call for ordinary mailbox use.
      required: [tool, arguments]
      properties:
        tool:
          type: string
          enum: [read_messages, get_message, wait_for_email]
        arguments:
          type: object
          additionalProperties: true

    MailboxQuickstart:
      type: object
      description: >
        MCP-first, read-only next calls for the verified signup inbox. Use these
        instead of inventing REST routes or parsing raw HTTP response files.
      required: [inbox, list_mail, read_message, wait_for_mail]
      properties:
        inbox: { type: string, format: email }
        list_mail: { $ref: "#/components/schemas/MailboxQuickstartCall" }
        read_message: { $ref: "#/components/schemas/MailboxQuickstartCall" }
        wait_for_mail: { $ref: "#/components/schemas/MailboxQuickstartCall" }

    VerifyResult:
      type: object
      description: >
        OTP verification activates the free account and returns the replacement
        full-permission key plus the ready `free.extrovertmail.com` inbox; the
        verification-only bootstrap key was revoked atomically. The inbox quickstart
        makes the response a self-contained handoff after a process restart or context
        compaction.
      required: [agent_id, agent_key, key_prefix, scopes, address, verified, message, mailbox_quickstart]
      properties:
        agent_id: { type: string }
        agent_key: { type: string, description: New full-permission key, shown once. }
        key_prefix: { type: string }
        scopes:
          type: array
          items: { type: string }
        address: { type: string, format: email, description: "The signup inbox, ready for immediate use." }
        verified: { type: boolean }
        message: { type: string }
        mailbox_quickstart: { $ref: "#/components/schemas/MailboxQuickstart" }
        org_claim_token:
          type: string
          description: >
            One-time, email-bound owner claim seed for the signup org. Present only
            on a fresh signup. In the console, sign in with the verified email and
            call GET /v1/admin/me to find the matching pending_signup_claims entry.
            Then POST /v1/admin/signup-claims/{claim_id}/accept to bind that identity
            to the signup org as owner.

    CreateInboxRequest:
      type: object
      properties:
        username:
          type: string
          description: >
            Local part. On `extrovertmail.com` and `free.extrovertmail.com` it is
            normalized to lowercase, must contain at least five characters, and must
            not be postmaster, admin, webadmin, legal, fraudmark, fraudmarc, keith,
            melissa, richard, sydney, syd, john, or johnny. These restrictions do not
            apply to customer-owned domains. Omit it for a generated local part.
        domain:
          type: string
          description: Must be within the key's allowed domains. Omit for the account's plan-appropriate shared domain.
        display_name: { type: string }
        webhook_url: { type: string, description: Register an inbound webhook for this inbox. }
        metadata:
          type: object
          nullable: true
          description: >
            Optional arbitrary key-value metadata to store on the inbox (AgentMail
            parity). Values may be string, number, or boolean. Caps: ≤256 keys, ≤256
            chars per key, ≤256 chars per string value. Nested objects and arrays are
            rejected. Echoed back (and replayed on idempotent retries) on the create
            response.
          additionalProperties:
            oneOf:
              - { type: string }
              - { type: number }
              - { type: boolean }
        project_id:
          type: string
          description: >
            Optional assertion that must match the key's bound project; NEVER a
            selector. A mismatch is 403. The inbox is always created in the key's
            stored project.
        client_id:
          type: string
          description: >
            Optional idempotency key (alias for the `Idempotency-Key` header).
            Re-creating with the same key returns the existing inbox rather than a
            duplicate; the same key with a different body returns 409.
        return_credentials:
          type: boolean
          default: false
          description: >
            Return the one-time IMAP/SMTP password on a newly created inbox.
            Requires the mailbox:credentials scope and a paid account. Omit this
            on ordinary agent/MCP creates.

    UpdateInboxRequest:
      type: object
      description: >
        Cheap, in-place inbox settings an owning agent may change without
        delete+recreate. Every field is optional; an omitted field leaves the
        stored value untouched (PATCH semantics). Sending an empty string clears a
        display/webhook field (display name falls back to the local part at the mail
        layers).
      properties:
        display_name:
          type: string
          description: Sender display / "From" name. Propagated to the inbox and the authenticated sender.
        webhook_url:
          type: string
          description: Replace the inbox's inbound webhook target (empty string clears it).
        daily_send_limit:
          type: integer
          minimum: 1
          maximum: 10000
          description: >
            Replace the inbox's rolling-24h recipient cap. Requires mailbox:quota
            in addition to mailbox:read; ordinary create/send keys cannot raise
            their own safety throttle.
        metadata:
          type: object
          nullable: true
          description: >
            Patch the inbox's arbitrary metadata (AgentMail parity). Shallow merge:
            an omitted `metadata` leaves it unchanged; an object merges into the
            existing metadata; a key whose value is null deletes that key; a
            top-level `metadata: null` clears ALL metadata (the response then carries
            `{}`). Values may be string, number, or boolean; a per-key `null` value
            DELETES that key (the documented delete mechanism); nested objects/arrays
            are rejected; the same ≤256 key/length caps as create apply.
          additionalProperties:
            oneOf:
              - { type: string }
              - { type: number }
              - { type: boolean }
              - { type: "null" }
        project_id:
          type: string
          description: >
            Optional assertion that must match the key's bound project; NEVER a
            selector. A mismatch is 403.

    PurchaseRequest:
      type: object
      description: x402 paid-purchase body. The buyer-supplied amount is never trusted; price is server-computed.
      properties:
        quantity: { type: integer, description: Number of mailboxes to provision. }
        domain: { type: string }
        display_name: { type: string }
        webhook_url: { type: string }

    PurchaseResult:
      type: object
      properties:
        payment_id: { type: string }
        settlement_ref: { type: string }
        inboxes:
          type: array
          items: { $ref: "#/components/schemas/Inbox" }

    RegisterWebhookRequest:
      type: object
      required: [url]
      properties:
        url: { type: string, format: uri }
        events:
          type: array
          items:
            type: string
            enum: [message.received]
            example: message.received
        event_types:
          type: array
          description: AgentMail-compatible alias for `events`.
          items:
            type: string
            enum: [message.received]
            example: message.received
        inbox: { type: string, description: Scope to one mailbox; omit for all. }
        address: { type: string, description: Legacy alias for `inbox`. }
        client_id:
          type: string
          description: >
            Optional idempotency key (alias for the `Idempotency-Key` header). A
            retry with the same key replays the original webhook registration.

    AdminRegisterWebhookRequest:
      allOf:
        - $ref: "#/components/schemas/RegisterWebhookRequest"
        - type: object
          required: [agent_id]
          properties:
            agent_id:
              type: string
              description: Active agent that owns the webhook registration.

    UpdateWebhookRequest:
      type: object
      description: >
        PATCH body for a webhook. Every field is optional; an omitted field leaves
        the stored value unchanged (PATCH semantics). The signing secret and id are
        immutable. `inbox` empty-string clears the per-inbox filter (covers all
        inboxes the agent owns).
      properties:
        url: { type: string, format: uri }
        events:
          type: array
          items:
            type: string
            enum: [message.received]
            example: message.received
        event_types:
          type: array
          description: AgentMail-compatible alias for `events`.
          items:
            type: string
            enum: [message.received]
            example: message.received
        inbox: { type: string, description: Scope to one mailbox; empty string clears the filter. }
        address: { type: string, description: Legacy alias for `inbox`. }
        active: { type: boolean, description: Enable or disable delivery without deleting. }

    Webhook:
      type: object
      description: >
        A webhook registration. `secret` is present only on the create response
        (shown once); list/get redact it. `inbox` is null when the webhook covers
        every inbox.
      properties:
        id: { type: string }
        url: { type: string }
        events:
          type: array
          items:
            type: string
            enum: [message.received]
        inbox: { type: string, nullable: true }
        agent_id: { type: string, description: Agent that owns the webhook. }
        secret: { type: string, description: HMAC signing secret. Create response only. }
        secret_prefix: { type: string }
        active: { type: boolean }
        created_at: { type: string, format: date-time }

    WebhookPage:
      type: object
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/Webhook" }
        total: { type: integer }

    AddContactListRequest:
      type: object
      description: >
        Add one allow/block entry to an inbox's contact lists. `pattern` is a bare
        email address (matched in full) or a bare domain (matches any address in
        that domain). A block entry rejects a matching recipient; when any allow
        entry exists, sends are restricted to recipients that match one (allowlist
        mode).
      required: [kind, pattern]
      properties:
        kind:
          type: string
          enum: [allow, block]
          description: Whether this entry permits (allow) or rejects (block) a match.
        direction:
          type: string
          enum: [send, receive]
          default: send
          description: >
            Traffic direction the entry governs. Only `send` is enforced today (at
            outbound time); `receive` is accepted for forward-compatibility.
        pattern:
          type: string
          description: A bare email address or a bare domain (e.g. `acme.com`).

    ContactListEntry:
      type: object
      description: >
        One allow/block contact-list entry. `inbox` is null when the entry is
        account-wide (covers every inbox the agent owns).
      properties:
        id: { type: string }
        inbox: { type: string, nullable: true }
        kind: { type: string, enum: [allow, block] }
        direction: { type: string, enum: [send, receive] }
        pattern: { type: string }
        created_at: { type: string, format: date-time }

    ContactListPage:
      type: object
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/ContactListEntry" }
        total: { type: integer }

    StreamEvent:
      type: object
      description: >
        One event from the SSE stream, carried as the `data:` payload of an
        `text/event-stream` frame. It is the SAME envelope a webhook delivers, so a
        stream consumer and a webhook consumer see byte-identical data. The SSE
        frame's `id:` field carries the monotonic resume token (pass it back as
        `Last-Event-ID` on reconnect) and its `event:` field carries `event` below.
      properties:
        event:
          type: string
          description: Event type, e.g. `message.received`.
          example: message.received
        id: { type: string, description: Opaque event id (`evt_...`). }
        created_at: { type: string, format: date-time }
        inbox: { type: string, description: The inbox this event concerns. }
        message: { $ref: "#/components/schemas/Message" }

    # --- admin plane ---

    IssueEnrollmentTokenRequest:
      type: object
      properties:
        label: { type: string }
        scopes:
          type: array
          items: { type: string }
        allowed_domains:
          type: array
          items: { type: string }
        max_mailboxes: { type: integer }
        daily_send_limit: { type: integer }
        reusable: { type: boolean }
        expires_in_hours: { type: integer }

    IssueEnrollmentTokenResult:
      type: object
      properties:
        id: { type: string }
        token: { type: string, description: Raw enrollment token, shown once. }
        prefix: { type: string }
        scopes:
          type: array
          items: { type: string }
        max_mailboxes: { type: integer }
        daily_send_limit: { type: integer }

    EnrollmentTokenSummary:
      type: object
      properties:
        id: { type: string }
        label: { type: string }
        prefix: { type: string }
        scopes:
          type: array
          items: { type: string }
        max_mailboxes: { type: integer }
        daily_send_limit: { type: integer }
        used_count: { type: integer }
        reusable: { type: boolean }
        revoked: { type: boolean }
        created_at: { type: string, format: date-time }

    EnrollmentTokenList:
      type: object
      properties:
        tokens:
          type: array
          items: { $ref: "#/components/schemas/EnrollmentTokenSummary" }
        next_page: { type: string }

    IssueAgentKeyRequest:
      type: object
      required: [agent_id, scopes]
      properties:
        agent_id: { type: string }
        scopes:
          type: array
          items: { type: string }
        expires_in_hours: { type: integer, minimum: 0 }

    IssueAgentKeyResult:
      type: object
      properties:
        id: { type: string }
        agent_id: { type: string }
        agent_handle: { type: string }
        agent_name: { type: string }
        key: { type: string, description: Raw agent API key, shown once. }
        prefix: { type: string }
        scopes:
          type: array
          items: { type: string }
        key_tier: { type: string, enum: [org, project, inbox] }
        ceiling_inbox_id: { type: string }
        enrollment_token_id: { type: string }
        expires_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }

    AgentKeySummary:
      type: object
      properties:
        id: { type: string }
        agent_id: { type: string }
        agent_handle: { type: string }
        agent_name: { type: string }
        prefix: { type: string }
        scopes:
          type: array
          items: { type: string }
        key_tier: { type: string, enum: [org, project, inbox] }
        ceiling_inbox_id: { type: string }
        enrollment_token_id: { type: string }
        revoked: { type: boolean }
        expires_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        last_used_at: { type: string, format: date-time }

    AgentKeyList:
      type: object
      properties:
        keys:
          type: array
          items: { $ref: "#/components/schemas/AgentKeySummary" }
        next_page: { type: string }

    AgentSummary:
      type: object
      properties:
        id: { type: string }
        handle: { type: string }
        name: { type: string }
        status: { type: string, enum: [active, disabled] }
        mailbox_count: { type: integer, minimum: 0, description: Inboxes owned by this agent in the active project. }
        last_seen_at: { type: string, format: date-time, nullable: true, description: Latest authenticated API-key activity. }
        created_at: { type: string, format: date-time }

    CreateAgentRequest:
      type: object
      required: [name]
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
          description: Human-facing agent name. The server generates its technical handle.

    AgentList:
      type: object
      properties:
        agents:
          type: array
          items: { $ref: "#/components/schemas/AgentSummary" }
        next_page: { type: string }

    AgentStatusResult:
      type: object
      properties:
        status: { type: string, enum: [active, disabled] }
        agent_id: { type: string }

    RecordInstruction:
      type: object
      description: A DNS record the customer must add for domain onboarding.
      properties:
        name: { type: string }
        type: { type: string, example: TXT }
        value: { type: string }
        priority: { type: integer, nullable: true }
        ttl: { type: integer }

    OnboardDomainRequest:
      type: object
      required: [domain, mode]
      properties:
        domain: { type: string }
        mode: { type: string, enum: [ns_delegated, purchased] }

    OnboardDomainResult:
      type: object
      properties:
        domain: { type: string }
        mode: { type: string, enum: [shared, ns_delegated, manual, purchased] }
        verification_status: { type: string }
        zone_id: { type: string }
        provisioning_phase: { type: string }
        provisioning_error: { type: string }
        delegation_ns:
          type: array
          items: { $ref: "#/components/schemas/RecordInstruction" }
        records:
          type: array
          items: { $ref: "#/components/schemas/RecordInstruction" }
        instruction: { type: string }

    DomainSummary:
      type: object
      properties:
        id: { type: string }
        domain: { type: string }
        mode: { type: string }
        verification_status: { type: string }
        dkim_status: { type: string }
        shared: { type: boolean, description: True for the hosted inbox domain included with the current plan. }
        provisioning_phase: { type: string }
        provisioning_error: { type: string }

    DomainList:
      type: object
      properties:
        domains:
          type: array
          items: { $ref: "#/components/schemas/DomainSummary" }
        next_page: { type: string }

    OnboardAgentDomainRequest:
      type: object
      required: [domain]
      description: >
        Request body for the agent-facing `POST /v1/domains`. `ns_delegated` adds a
        domain the customer controls without spending. The legacy `purchased` input
        is accepted only as a compatibility shim and is converted to the human-controlled
        commerce request flow; it never approves, charges, or registers inline.
      properties:
        domain: { type: string }
        mode:
          type: string
          enum: [ns_delegated, purchased]
          description: Onboarding path. Defaults to ns_delegated. `purchased` is a deprecated compatibility input that creates a commerce request.
        scope:
          type: string
          enum: [org, project]
          description: >
            Domain visibility. Defaults to `org` (org-shared, usable by every project
            in the org). `project` binds the domain to the key's OWN bound project
            (never client-selected) so it can create inboxes only in that project.
            A legacy/unscoped key (no bound project) falls back to `org`.
        project_id:
          type: string
          description: >
            Optional assertion that must match the key's bound project; NEVER a
            selector. A mismatch is 403. The binding is always derived from the key.

    AgentDomain:
      type: object
      description: >
        The agent-facing view of one domain: its status plus the nameserver records
        the customer must publish for delegated setup. Records are empty on list
        reads and for hosted/purchased modes.
      properties:
        id: { type: string }
        domain: { type: string }
        mode: { type: string, enum: [shared, ns_delegated, manual, purchased] }
        verification_status: { type: string }
        dkim_status: { type: string }
        shared: { type: boolean }
        provisioning_phase: { type: string }
        provisioning_error: { type: string }
        created_at: { type: string, format: date-time }
        records:
          type: array
          items: { $ref: "#/components/schemas/RecordInstruction" }
        delegation_ns:
          type: array
          items: { $ref: "#/components/schemas/RecordInstruction" }
        instruction: { type: string }

    AgentDomainList:
      type: object
      description: A page of domains in the canonical agent-plane envelope.
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/AgentDomain" }
        total: { type: integer }
        next_cursor: { type: string }

    AuditEvent:
      type: object
      properties:
        id: { type: string }
        actor: { type: string }
        action: { type: string }
        target: { type: string }
        severity: { type: string, enum: [info, warn, error] }
        metadata:
          type: object
          additionalProperties: { type: string }
        created_at: { type: string, format: date-time }

    AuditList:
      type: object
      properties:
        events:
          type: array
          items: { $ref: "#/components/schemas/AuditEvent" }
        next_page: { type: string }

    OrgSettings:
      type: object
      properties:
        org_name: { type: string }
        default_domain: { type: string }
        default_onboarding_mode: { type: string }
        default_mailbox_cap: { type: integer }
        webhook_signing_prefix: { type: string }
        x402_mode: { type: string }
        early_access: { type: boolean }

    PatchOrgSettingsRequest:
      type: object
      description: Any subset of the org settings; omitted fields are unchanged.
      properties:
        org_name: { type: string }
        default_domain: { type: string }
        default_onboarding_mode: { type: string }
        default_mailbox_cap: { type: integer }
        webhook_signing_prefix: { type: string }
        x402_mode: { type: string }
        early_access: { type: boolean }

    AdminInbox:
      type: object
      properties:
        address: { type: string }
        agent_id: { type: string }
        domain: { type: string }
        webhook_url: { type: string }
        daily_send_used: { type: integer, description: Recipients sent in the rolling 24-hour window. }
        daily_send_cap: { type: integer }
        direct_smtp_enabled:
          type: boolean
          description: >
            Whether this inbox is configured for direct SMTP outside the review
            pipeline. Effective access also requires an active paid entitlement.
        created_at: { type: string, format: date-time }

    AdminCreateInboxRequest:
      type: object
      required: [agent_id]
      properties:
        username:
          type: string
          description: >
            Local part. On `extrovertmail.com` and `free.extrovertmail.com` it is
            normalized to lowercase, must contain at least five characters, and must
            not be postmaster, admin, webadmin, legal, fraudmark, fraudmarc, keith,
            melissa, richard, sydney, syd, john, or johnny. These restrictions do not
            apply to customer-owned domains. Omit it for a generated local part.
        domain_id: { type: string, description: Domain id from the console domains list. }
        domain: { type: string, description: Domain name fallback when domain_id is omitted. }
        agent_id: { type: string, description: Active agent that owns the new inbox. }
        display_name: { type: string }
        webhook_url: { type: string }

    AdminUpdateInboxRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        daily_send_limit: { type: integer, minimum: 1, maximum: 10000 }
        direct_smtp_enabled:
          type: boolean
          description: >
            Explicit per-inbox raw SMTP bypass. It is disabled by default and can be
            enabled only by a human for an account with an active paid entitlement.
            Disabling is always allowed. Agent API, SDK, and MCP sends continue through
            the normal review controls regardless of this setting.

    AdminInboxList:
      type: object
      properties:
        inboxes:
          type: array
          items: { $ref: "#/components/schemas/AdminInbox" }
        next_page: { type: string }

    AdminMessage:
      type: object
      description: A source-preserving message shape for the console plane.
      properties:
        id: { type: string }
        seq: { type: string }
        thread_id: { type: string }
        direction: { type: string, enum: [inbound, outbound] }
        from: { type: string }
        to: { type: string }
        cc: { type: string }
        reply_to: { type: string }
        subject: { type: string }
        date: { type: string }
        message_id: { type: string }
        in_reply_to: { type: string }
        references: { type: string }
        seen: { type: boolean }
        has_attachments: { type: boolean }
        body: { type: string }
        text: { type: string }
        html: { type: string }
        extracted_text: { type: string }
        extracted_html: { type: string }

    AdminMessageList:
      type: object
      properties:
        messages:
          type: array
          items: { $ref: "#/components/schemas/AdminMessage" }

    AdminMessageDetail:
      allOf:
        - $ref: "#/components/schemas/Message"
        - type: object
          properties:
            attachments:
              type: array
              items: { $ref: "#/components/schemas/Attachment" }

    AdminThreadList:
      type: object
      description: A cursor page of persistent thread summaries for one inbox.
      properties:
        threads:
          type: array
          items: { $ref: "#/components/schemas/AdminUnifiedThread" }
        total: { type: integer }
        next_cursor: { type: string }

    AdminThreadDetail:
      type: object
      properties:
        thread_id: { type: string }
        subject: { type: string }
        messages:
          type: array
          items: { $ref: "#/components/schemas/AdminMessage" }

    AdminUnifiedThread:
      type: object
      description: One row in the shared-inbox feed, tagged with its owning inbox.
      properties:
        id: { type: string }
        inbox: { type: string }
        subject: { type: string }
        participants:
          type: array
          items: { type: string }
        message_count: { type: integer }
        last_message_at: { type: string }
        snippet: { type: string }
        unread: { type: boolean }
        last_message_id: { type: string }
        last_message_has_attachments: { type: boolean }

    AdminUnifiedThreadFeed:
      type: object
      description: Stable keyset page merged newest-first across the active project's inboxes.
      properties:
        threads:
          type: array
          items: { $ref: "#/components/schemas/AdminUnifiedThread" }
        total: { type: integer }
        inbox_count: { type: integer }
        next_cursor: { type: string, description: Opaque global keyset cursor; pass back unchanged. }
        partial_failure: { type: boolean }
        failed_inboxes:
          type: array
          items: { type: string }

    StatusResult:
      type: object
      properties:
        status: { type: string }
        message_id:
          type: string
          description: >
            A stable identifier for the submitted message the sender can
            self-correlate on: the opaque msg_ id when the Sent append resolved,
            else the bracketed RFC5322 Message-ID header value. Present on the
            direct-send response; always non-empty when status is "sent".
        review_id:
          type: string
          description: >
            Opaque review id (rr_…) for the review row that governed this send.
            ADDITIVE; see SendResult.review_id.

    ReviewLink:
      type: object
      description: >
        A BYO review-agent link (rvl_…); the subscription that routes a reviewed
        draft to a linked review-agent (D5; §9). One link per scope: inbox_address set
        = that inbox, empty = account-wide (per-inbox overrides account-wide). The
        reviewer holds review:act, NEVER mailbox:send; on approve/edit the platform
        sends with the COMPOSER's creds (credential boundary). hmac_secret is
        write-only; reads return has_secret only. Opaque ids only (D10).
      required: [id, reviewer_agent_id, transport, escalate_after_s, max_hops, review_deadline_s, active, consec_failures, has_secret, created_at, updated_at]
      properties:
        id: { type: string, description: Opaque review-link id (rvl_…). }
        inbox_address: { type: string, description: "Scope: a specific inbox; empty = account-wide." }
        reviewer_agent_id: { type: string, description: The linked reviewer agent (agt_…). }
        transport: { type: string, enum: [webhook, sse, websocket] }
        webhook_url: { type: string, description: "Delivery URL (webhook transport only)." }
        has_secret: { type: boolean, description: "Whether an hmac signing secret is configured (the secret itself is never echoed)." }
        escalate_after_s: { type: integer, description: "Seconds before the reviewer-liveness sweeper auto-escalates to the human (default 300)." }
        max_hops: { type: integer, description: "Circuit breaker: max reviewer hand-backs before the next action is forced to the human (default 3)." }
        review_deadline_s: { type: integer, description: "Circuit breaker: hard per-review deadline in seconds (default 86400)." }
        active: { type: boolean }
        consec_failures: { type: integer, description: "Consecutive delivery failures; an unhealthy link is skipped by routing until recovery." }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ReviewLinkList:
      type: object
      required: [review_links]
      properties:
        review_links: { type: array, items: { $ref: "#/components/schemas/ReviewLink" } }

    CreateReviewLinkRequest:
      type: object
      description: >
        Link a reviewer agent to an inbox (or account-wide). transport defaults to
        webhook; a webhook transport REQUIRES webhook_url. The timing knobs default at
        the server when omitted (escalate_after_s=300, max_hops=3, review_deadline_s=
        86400). One link per scope; a duplicate scope is 409.
      required: [reviewer_agent_id]
      properties:
        inbox_address: { type: string, description: "Empty = account-wide." }
        reviewer_agent_id: { type: string, description: The reviewer agent (agt_…), must belong to the customer. }
        transport: { type: string, enum: [webhook, sse, websocket], description: "Defaults webhook." }
        webhook_url: { type: string, description: "Required for the webhook transport." }
        hmac_secret: { type: string, description: "Write-only signing secret for webhook deliveries." }
        escalate_after_s: { type: integer }
        max_hops: { type: integer }
        review_deadline_s: { type: integer }

    DeleteReviewLinkResult:
      type: object
      description: >
        The delete outcome plus how many in-flight reviews were swept back to the human
        queue (no orphan deliveries; §9).
      required: [deleted, swept]
      properties:
        deleted: { type: boolean }
        swept: { type: integer, description: "In-flight reviews returned to the human queue by the delete." }

    ReviewDecisionContext:
      type: object
      description: >
        The reviewer's read-only decision surface for a review (D5; §9): the intent +
        current draft + thread + the two-circuit-breaker budget. force_to_human is true
        when EITHER breaker has tripped (hop_count ≥ max_hops, or the hard
        review_deadline); the reviewer's next reject would be FORCED to the human
        regardless of intent (the human is the only terminal authority, D17).
      required: [review, turns, hop_count, max_hops, review_deadline, deadline_passed, hops_exhausted, force_to_human]
      properties:
        review: { $ref: "#/components/schemas/Review" }
        turns: { type: array, items: { $ref: "#/components/schemas/ReviewTurn" } }
        hop_count: { type: integer, description: "Circuit breaker (a): reviewer hand-backs so far." }
        max_hops: { type: integer, description: "Circuit breaker (a): the ceiling; at hop_count ≥ max_hops the next action is forced to the human." }
        review_deadline: { type: string, format: date-time, description: "Circuit breaker (b): the hard per-review wall-clock deadline (created_at + review_deadline_s)." }
        deadline_passed: { type: boolean, description: "Breaker (b) tripped: now ≥ review_deadline." }
        hops_exhausted: { type: boolean, description: "Breaker (a) tripped: hop_count ≥ max_hops." }
        force_to_human: { type: boolean, description: "Either breaker tripped: a reject is overridden to a human escalation." }
        force_reason: { type: string, description: "The tripped breaker (max_hops_reached | review_deadline_passed)." }

    ReviewerDecisionRequest:
      type: object
      description: >
        A reviewer decision. action is approve|edit|reject|escalate. revision + version
        are the optimistic CAS (a mismatch is 409 STALE with NO mutation; the human
        always wins, D17). subject/body carry the edited content for the edit action.
        feedback is the reviewer's note (reject: the rule-birth signal; escalate: the
        human-facing escalation reason).
      required: [action, revision]
      properties:
        action: { type: string, enum: [approve, edit, reject, escalate] }
        revision: { type: integer, description: "The revision you decided against (PRIMARY CAS)." }
        version: { type: integer, description: "Optional row-version CAS (defense in depth)." }
        subject: { type: string, description: "Edited subject (edit action)." }
        body: { type: string, description: "Edited body text (edit action)." }
        feedback: { type: string, description: "Reviewer note (reject: rule-birth signal; escalate: human-facing reason)." }

    ReviewerDecisionResult:
      type: object
      description: >
        The reviewer-decision outcome. kind=sent when the platform sent with the
        COMPOSER's creds (approve/edit); kind=sent_to_human when the draft returned to the
        human queue (reject/escalate, or a reject forced to the human by a circuit
        breaker). forced_by_breaker names the tripped breaker when one overrode the intent.
      required: [kind, review, sent, sent_to_human]
      properties:
        kind: { type: string, enum: [sent, sent_to_human] }
        review: { $ref: "#/components/schemas/Review" }
        sent: { type: boolean, description: "True iff the platform sent with the composer's creds." }
        message_id: { type: string, description: "The delivered message id when sent." }
        thread_id: { type: string, description: "The thread id when sent on the reply path." }
        sent_to_human: { type: boolean, description: "True iff the draft returned to the human queue." }
        forced_by_breaker: { type: string, description: "The tripped circuit breaker (max_hops_reached | review_deadline_passed) when one forced the human." }

    # --- console admin: orgs / projects / members / invitations / session ---

    AdminMe:
      type: object
      description: >
        The authenticated console identity's reachable orgs + projects (GET
        /v1/admin/me). IDENTITY-ONLY: the reachable set is derived from the user's
        membership rows, never from a preselected/cookie customer. The *_hint fields
        are landing hints only; the console revalidates them against
        reachable_orgs/projects_by_org before using them.
      required: [identity, last_project_by_org, reachable_orgs, projects_by_org, pending_signup_claims]
      properties:
        identity: { $ref: "#/components/schemas/AdminIdentity" }
        active_org_hint: { type: string, description: Landing hint (the user's default org). }
        active_project_hint: { type: string, description: Landing hint (the user's default project). }
        last_project_by_org:
          type: object
          description: "Per-org remembered/default project (orgId -> projectId)."
          additionalProperties: { type: string }
        reachable_orgs:
          type: array
          items: { $ref: "#/components/schemas/AdminReachableOrg" }
        projects_by_org:
          type: object
          description: The active projects for each reachable org, keyed by org id.
          additionalProperties:
            type: array
            items: { $ref: "#/components/schemas/AdminProjectSummary" }
        pending_signup_claims:
          type: array
          description: >
            Live system-seeded owner claims matching the authenticated identity's
            verified email. Claim ids are selectors, not bearer credentials; no
            invitation token or hash material is exposed.
          items: { $ref: "#/components/schemas/PendingSignupClaim" }

    PendingSignupClaim:
      type: object
      required: [id, org_id, role, created_at, expires_at]
      properties:
        id: { type: string }
        org_id: { type: string }
        role: { type: string, enum: [owner] }
        created_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }

    AdminIdentity:
      type: object
      properties:
        clerk_user_id: { type: string }
        email: { type: string }

    AdminReachableOrg:
      type: object
      description: One org the identity can reach, with the effective role + source.
      required: [id, customer_id, display_name, root_org_id, role, source, depth, breadcrumb, plan_tier, limits]
      properties:
        id: { type: string, description: Canonical org UUID. }
        customer_id: { type: string, description: Compatibility customer id linked to the org. }
        display_name: { type: string }
        parent_org_id: { type: string, nullable: true, description: Null for a root org. }
        root_org_id: { type: string }
        role: { type: string, enum: [owner, admin, member, viewer, billing] }
        source: { type: string, enum: [direct, inherited, msp_grant], description: How the identity reaches this org. }
        depth: { type: integer, description: Distance from the root org (0 = root). }
        breadcrumb:
          type: array
          items: { type: string }
        plan_tier: { type: string }
        limits: { $ref: "#/components/schemas/AdminOrgLimits" }

    AdminOrgLimits:
      type: object
      properties:
        max_child_orgs: { type: integer }
        max_projects: { type: integer }

    AdminProjectSummary:
      type: object
      description: A project summary as rendered in the project switcher.
      required: [id, name, slug, is_default]
      properties:
        id: { type: string, description: Canonical project UUID. }
        name: { type: string }
        slug: { type: string }
        is_default: { type: boolean }

    Org:
      type: object
      description: >
        An Extrovert organization (the authority/billing boundary). org ids are
        plain server-created UUIDs (never client-supplied). A root org has a null
        parent_org_id; child orgs reference their parent.
      required: [id, customer_id, root_org_id, display_name, slug, lifecycle_status, plan_tier, max_child_orgs, max_projects, version, created_at, updated_at]
      properties:
        id: { type: string }
        customer_id: { type: string }
        parent_org_id: { type: string, nullable: true }
        root_org_id: { type: string }
        display_name: { type: string }
        slug: { type: string }
        lifecycle_status: { type: string, enum: [active, archived, deleted] }
        plan_tier: { type: string }
        max_child_orgs: { type: integer }
        max_projects: { type: integer }
        version: { type: integer, description: Optimistic-CAS version. }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    OrgList:
      type: object
      properties:
        orgs:
          type: array
          items: { $ref: "#/components/schemas/Org" }

    AdminBilling:
      type: object
      required: [org_id, customer_id, billing_owner_id, stripe_mode, stripe_configured, plan_tier, entitlements, usage, plans]
      properties:
        org_id: { type: string }
        customer_id: { type: string }
        billing_owner_id: { type: string, description: The org id used for plan and monthly usage enforcement. }
        stripe_mode: { type: string, enum: [test, live] }
        stripe_configured: { type: boolean }
        plan_tier: { type: string, enum: [free, developer, startup, enterprise] }
        entitlements: { $ref: "#/components/schemas/AdminBillingPlan" }
        subscription: { $ref: "#/components/schemas/AdminBillingSubscription" }
        usage: { $ref: "#/components/schemas/AdminBillingUsage" }
        plans:
          type: array
          items: { $ref: "#/components/schemas/AdminBillingPlan" }

    AdminBillingPlan:
      type: object
      required: [tier, name, monthly_price_cents, domain_annual_com_reference_cents, included_messages_monthly, inbox_limit, custom_domain_limit, storage_bytes, pod_limit, webhook_limit, member_limit]
      properties:
        tier: { type: string, enum: [free, developer, startup, enterprise] }
        name: { type: string }
        monthly_price_cents: { type: integer, format: int64 }
        domain_annual_com_reference_cents: { type: integer, format: int64, description: Reference annual managed-domain price for .com; other TLDs are quoted from live registrar cost at checkout. }
        included_messages_monthly: { type: integer, format: int64, description: Combined inbound plus outbound recipient/message quota. }
        daily_message_cap: { type: integer, format: int64 }
        inbox_limit: { type: integer, format: int64 }
        custom_domain_limit: { type: integer, format: int64 }
        storage_bytes: { type: integer, format: int64, description: Decimal GB converted to bytes. }
        pod_limit: { type: integer, format: int64 }
        webhook_limit: { type: integer, format: int64 }
        member_limit: { type: integer, format: int64 }
        custom: { type: boolean }
        checkout_available: { type: boolean }

    AdminBillingSubscription:
      type: object
      required: [org_id, customer_id, plan_tier, status, cancel_at_period_end]
      properties:
        org_id: { type: string }
        customer_id: { type: string }
        plan_tier: { type: string, enum: [free, developer, startup, enterprise] }
        status: { type: string }
        stripe_customer_id: { type: string }
        stripe_subscription_id: { type: string }
        stripe_subscription_item_id: { type: string }
        stripe_price_id: { type: string }
        current_period_start: { type: string, format: date-time }
        current_period_end: { type: string, format: date-time }
        cancel_at_period_end: { type: boolean }

    AdminBillingUsage:
      type: object
      required: [billing_owner_id, customer_id, month, sent_messages, sent_recipients, received_messages, received_recipients, storage_bytes, storage_used_bytes, storage_peak_used_bytes, storage_included_bytes, storage_over_limit_bytes, storage_peak_over_limit_bytes, storage_mailbox_count, storage_quota_exceeded, total_messages, message_quota_limit, message_quota_remaining, message_quota_exceeded]
      properties:
        billing_owner_id: { type: string }
        customer_id: { type: string }
        month: { type: string, example: "2026-06" }
        sent_messages: { type: integer, format: int64 }
        sent_recipients: { type: integer, format: int64 }
        received_messages: { type: integer, format: int64 }
        received_recipients: { type: integer, format: int64 }
        storage_bytes: { type: integer, format: int64, description: Compatibility alias for storage_used_bytes. }
        storage_used_bytes: { type: integer, format: int64, description: Current owner-level storage gauge from latest mailbox snapshots. }
        storage_peak_used_bytes: { type: integer, format: int64, description: Billing-month high-water mark, summed from the peak observed bytes per durable storage object. }
        storage_included_bytes: { type: integer, format: int64, description: Included storage bytes for the effective plan; -1 means unlimited/custom. }
        storage_over_limit_bytes: { type: integer, format: int64 }
        storage_peak_over_limit_bytes: { type: integer, format: int64 }
        storage_mailbox_count: { type: integer }
        storage_captured_at: { type: string, format: date-time }
        storage_quota_exceeded: { type: boolean }
        total_messages: { type: integer, format: int64 }
        message_quota_limit: { type: integer, format: int64, description: Combined monthly inbound plus outbound message quota for the effective plan; -1 means unlimited/custom. }
        message_quota_remaining: { type: integer, format: int64, description: Remaining included messages this month; -1 means unlimited/custom. }
        message_quota_exceeded: { type: boolean }

    CreateBillingCheckoutRequest:
      type: object
      required: [plan_tier, success_url, cancel_url]
      properties:
        plan_tier: { type: string, enum: [developer, startup] }
        success_url: { type: string, format: uri }
        cancel_url: { type: string, format: uri }

    CreateBillingCheckoutResponse:
      type: object
      required: [id, url, status, plan_tier, mode, subscription]
      properties:
        id: { type: string, description: Stripe Checkout Session id. }
        url: { type: string, format: uri }
        status: { type: string }
        plan_tier: { type: string, enum: [developer, startup] }
        mode: { type: string, enum: [test, live] }
        subscription: { $ref: "#/components/schemas/AdminBillingSubscription" }

    CreateDomainBillingCheckoutRequest:
      type: object
      required: [domain, success_url, cancel_url]
      properties:
        domain: { type: string, example: "agent.agency" }
        success_url: { type: string, format: uri }
        cancel_url: { type: string, format: uri }

    CreateDomainBillingCheckoutResponse:
      type: object
      required: [id, url, status, mode, domain, plan_tier, currency, registrar_cost_cents, baseline_com_cost_cents, domain_annual_price_cents, stripe_price_id]
      properties:
        id: { type: string, description: Stripe Checkout Session id. }
        url: { type: string, format: uri }
        status: { type: string }
        mode: { type: string, enum: [test, live] }
        domain: { type: string }
        plan_tier: { type: string, enum: [developer, startup] }
        currency: { type: string, example: "usd" }
        registrar_cost_cents: { type: integer, format: int64 }
        baseline_com_cost_cents: { type: integer, format: int64 }
        domain_annual_price_cents: { type: integer, format: int64 }
        stripe_price_id: { type: string }

    CreateBillingPortalRequest:
      type: object
      required: [return_url]
      properties:
        return_url: { type: string, format: uri }

    CreateBillingPortalResponse:
      type: object
      required: [id, url, mode]
      properties:
        id: { type: string, description: Stripe Billing Portal Session id. }
        url: { type: string, format: uri }
        mode: { type: string, enum: [test, live] }

    StripeWebhookAck:
      type: object
      required: [status, inserted, event_id, event_type]
      properties:
        status: { type: string, enum: [ok] }
        inserted: { type: boolean }
        event_id: { type: string }
        event_type: { type: string }

    CreateOrgRequest:
      type: object
      description: >
        Create a CHILD org. parent_org_id is required (only child orgs are creatable
        from the console in v1; a user's single root org is bootstrapped on first
        sign-in). admin+ on the parent is required. Client-supplied org ids are NEVER
        accepted; the server creates a UUID and a compatibility customer id.
      required: [parent_org_id]
      properties:
        display_name: { type: string }
        slug: { type: string }
        parent_org_id: { type: string }

    PatchOrgRequest:
      type: object
      description: Update an org's display fields. Plan/limit fields are not console-patchable in v1.
      properties:
        display_name: { type: string }
        slug: { type: string }
        expected_version: { type: integer, description: Optimistic-CAS version of the row being updated. }

    Project:
      type: object
      description: >
        An Extrovert project (the resource namespace inside an org). project ids are
        plain server-created UUIDs (never client-supplied).
      required: [id, org_id, customer_id, name, slug, description, is_default, lifecycle_status, version, created_at, updated_at]
      properties:
        id: { type: string }
        org_id: { type: string }
        customer_id: { type: string }
        name: { type: string }
        slug: { type: string }
        description: { type: string }
        is_default: { type: boolean }
        lifecycle_status: { type: string, enum: [active, archived, deleted] }
        version: { type: integer, description: Optimistic-CAS version. }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    ProjectList:
      type: object
      properties:
        projects:
          type: array
          items: { $ref: "#/components/schemas/Project" }

    CreateProjectRequest:
      type: object
      description: >
        Create a project under the org. Enforced against the org's max_projects cap
        inside the same transaction as the create. Client-supplied ids are never
        accepted; the server creates a UUID.
      required: [name]
      properties:
        name: { type: string }
        slug: { type: string }
        description: { type: string }

    PatchProjectRequest:
      type: object
      description: Update a project's display fields.
      properties:
        name: { type: string }
        slug: { type: string }
        description: { type: string }
        expected_version: { type: integer, description: Optimistic-CAS version of the row being updated. }

    OrgMember:
      type: object
      description: One org membership row.
      required: [clerk_user_id, email, role, created_at, updated_at]
      properties:
        clerk_user_id: { type: string }
        email: { type: string }
        role: { type: string, enum: [owner, admin, member, viewer, billing] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    OrgMemberList:
      type: object
      properties:
        members:
          type: array
          items: { $ref: "#/components/schemas/OrgMember" }

    CreateInvitationRequest:
      type: object
      description: >
        Invite an email to the org. Role-capped: the inviter cannot grant a role
        above their own effective role. Idempotent: one pending invitation per
        (org, email). The raw token is returned ONCE on a fresh create.
      required: [email, role]
      properties:
        email: { type: string, format: email }
        role: { type: string, enum: [owner, admin, member, viewer, billing] }
        expires_in_days: { type: integer, description: Optional TTL in days. }

    UpdateMemberRoleRequest:
      type: object
      description: >
        Change an existing member's role. Role-capped: the actor cannot grant a role
        above their own effective role. Demoting the org's last owner is refused (409).
      required: [role]
      properties:
        role: { type: string, enum: [owner, admin, member, viewer, billing] }

    AcceptInvitationRequest:
      type: object
      description: >
        Accept an invitation with its raw token. IDENTITY-ONLY: the accept binds to
        the authenticated user's VERIFIED Clerk email, which must equal the
        normalized invitation email (else 403). Idempotent for the same user.
      required: [token]
      properties:
        token: { type: string }

    OrgInvitation:
      type: object
      description: >
        An org invitation. `token` is the raw secret returned ONCE on a fresh create
        (empty when an existing pending invitation was reused, or on accept/revoke).
      required: [id, org_id, email, role, status, token_prefix, expires_at, created_at]
      properties:
        id: { type: string }
        org_id: { type: string }
        email: { type: string }
        role: { type: string, enum: [owner, admin, member, viewer, billing] }
        status: { type: string, enum: [pending, accepted, revoked, expired] }
        token_prefix: { type: string }
        token: { type: string, description: Raw invitation token; present only on a fresh create. }
        expires_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }

    CommerceBlocker:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }
        scope: { type: string }
        limit_id: { type: string }
        used_cents: { type: integer, format: int64 }
        reserved_cents: { type: integer, format: int64 }
        limit_cents: { type: integer, format: int64 }
        requested_cents: { type: integer, format: int64 }
        used_count: { type: integer, format: int64 }
        reserved_count: { type: integer, format: int64 }
        limit_count: { type: integer, format: int64 }
        reset_at: { type: string, format: date-time }
        manage_url: { type: string, format: uri }
    DomainQuoteRequest:
      type: object
      required: [domain]
      properties:
        domain: { type: string, example: example.com }
    DomainQuote:
      type: object
      required: [object, domain, available, currency, quote_cents, renewal_cents, premium, quote_expires_at, blockers]
      properties:
        object: { type: string, enum: [domain_quote] }
        domain: { type: string }
        available: { type: boolean }
        currency: { type: string, example: usd }
        quote_cents: { type: integer, format: int64 }
        renewal_cents: { type: integer, format: int64 }
        premium: { type: boolean }
        quote_expires_at: { type: string, format: date-time }
        required_plan: { type: string, enum: [developer, startup, enterprise] }
        required_plan_price_cents: { type: integer, format: int64, description: Maximum monthly price for the required plan; any actual immediate plan charge is prorated and must not exceed this amount. }
        blockers: { type: array, items: { $ref: "#/components/schemas/CommerceBlocker" } }
    CreateDomainPurchaseRequest:
      type: object
      required: [domain]
      properties:
        domain: { type: string }
        scope: { type: string, enum: [org, project], default: org }
        project_id: { type: string }
        rationale: { type: string, maxLength: 2000 }
        auto_renew: { type: boolean, default: true }
    CreatePlanChangeRequest:
      type: object
      required: [target_plan]
      properties:
        target_plan: { type: string, enum: [free, developer, startup] }
        project_id: { type: string, description: Required on the console creation route; agent routes derive it from the key. }
        rationale: { type: string, maxLength: 2000 }
    CommerceRequest:
      type: object
      required: [object, id, kind, state, currency, quote_cents, renewal_cents, auto_renew, blockers, agent_next_action, retry_safe, poll_after_seconds, version, created_at, updated_at]
      properties:
        object: { type: string, enum: [commerce_request] }
        id: { type: string }
        project_id: { type: string }
        agent_id: { type: string, description: Present when an agent initiated the request. }
        kind: { type: string, enum: [domain_purchase, plan_change] }
        state: { type: string }
        domain: { type: string }
        domain_scope: { type: string, enum: [org, project] }
        target_plan: { type: string }
        current_plan: { type: string }
        rationale: { type: string }
        currency: { type: string }
        quote_cents: { type: integer, format: int64 }
        renewal_cents: { type: integer, format: int64 }
        approved_max_cents: { type: integer, format: int64 }
        quote_expires_at: { type: string, format: date-time }
        auto_renew: { type: boolean }
        required_plan: { type: string }
        required_plan_price_cents: { type: integer, format: int64 }
        blocker_code: { type: string }
        blockers: { type: array, items: { $ref: "#/components/schemas/CommerceBlocker" } }
        approval_url: { type: string, format: uri }
        payment_action_url: { type: string, format: uri }
        external_job_id: { type: string }
        effective_at: { type: string, format: date-time }
        agent_next_action: { type: string }
        retry_safe: { type: boolean }
        poll_after_seconds: { type: integer }
        version: { type: integer, format: int64 }
        notification_state: { type: string }
        notification_last_error: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    CommerceRequestPage:
      type: object
      required: [items, total]
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/CommerceRequest" } }
        total: { type: integer }
        next_cursor: { type: string }
    CommerceSpendLimitInput:
      type: object
      required: [scope, name, cadence, budget_cents, budget_enabled, per_purchase_max_cents, per_purchase_enabled, domain_count_limit, domain_count_enabled, allow_premium]
      properties:
        expected_version: { type: integer, format: int64 }
        scope: { type: string, enum: [org, project, agent] }
        project_id: { type: string }
        agent_id: { type: string }
        name: { type: string }
        currency: { type: string, default: usd }
        cadence: { type: string, enum: [does_not_repeat, weekly, monthly, quarterly, annually] }
        budget_cents: { type: integer, format: int64, minimum: 0 }
        budget_enabled: { type: boolean }
        per_purchase_max_cents: { type: integer, format: int64, minimum: 0 }
        per_purchase_enabled: { type: boolean }
        domain_count_limit: { type: integer, format: int64, minimum: 0 }
        domain_count_enabled: { type: boolean }
        allowed_tlds: { type: array, items: { type: string } }
        allow_premium: { type: boolean }
        active: { type: boolean }
        expires_at: { type: string, format: date-time }
    CommerceSpendLimit:
      allOf:
        - $ref: "#/components/schemas/CommerceSpendLimitInput"
        - type: object
          required: [id, org_id, currency, period_start, spent_cents, reserved_cents, purchased_count, reserved_count, active, version, created_at, updated_at]
          properties:
            id: { type: string }
            org_id: { type: string }
            period_start: { type: string, format: date-time }
            period_end: { type: string, format: date-time }
            spent_cents: { type: integer, format: int64 }
            reserved_cents: { type: integer, format: int64 }
            purchased_count: { type: integer, format: int64 }
            reserved_count: { type: integer, format: int64 }
            version: { type: integer, format: int64 }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
    DecideCommerceRequest:
      type: object
      required: [expected_version]
      properties:
        expected_version: { type: integer, format: int64 }
        approved_max_cents: { type: integer, format: int64, minimum: 0 }
        auto_renew: { type: boolean }
        reason: { type: string }
        future_authority: { $ref: "#/components/schemas/CommerceSpendLimitInput" }

  responses:
    Unauthorized:
      description: Missing or invalid credential.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Forbidden:
      description: Authenticated but lacking the required scope, or out of quota.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: Resource not found.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    BadRequest:
      description: Invalid request.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Conflict:
      description: Resource conflict (e.g. a duplicate entry).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    IdempotencyConflict:
      description: >
        The supplied idempotency key was already used with a different request body
        (`error` = `idempotency_conflict`).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    RateLimited:
      description: Per-key rate limit exceeded. Carries a `Retry-After` header.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds until the window resets.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    ServiceUnavailable:
      description: A required capability is not configured (e.g. buy-through-us).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotConfigured:
      description: A required integration or server-side capability is not configured.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    BadGateway:
      description: Upstream delivery failed. The request can be retried.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    SendBadRequest:
      description: >
        The send/reply/forward is malformed or fails validation. Decode and
        validation failures are problem+json (`code`: `bad_request`) and NAME the
        offending field; an unknown key lists the accepted set, and supplying both
        `text` and the deprecated `body` with DIFFERENT content is
        `errors[0].code = conflicting_alias` (there is no safe guess, so the request
        is refused rather than relayed with the wrong bytes). A few pre-existing
        domain rejections reached through this path (e.g. `domain_not_allowed`)
        still carry the legacy `{error, message}` envelope.
      content:
        application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } }
        application/json: { schema: { $ref: "#/components/schemas/Error" } }
    IntentRequired:
      description: >
        The resolved review mode is `review` but no intent summary was supplied
        (Problem `code` = `intent_required`; D3). Nothing was sent and nothing was
        queued; `detail` names the exact field to add and re-send.
      content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } }
    ReviewConflict:
      description: >
        A Review Loop conflict, as problem+json. Branch on `code`, NOT on the 409
        status; the four codes demand opposite behavior. `stale`: the
        revision/version you named is no longer current (a human moved the draft) and
        NOTHING was mutated; `errors[]` carries the current `state`, `revision` and
        `version`, so re-apply your change on top and resubmit with the new
        `parent_revision` (retry, bounded to ~3). `born_stale`: the redraft was built
        against an older rule high-water; re-read the rules and resubmit, or
        `restamp_review` if nothing genuinely changed (at most one retry per
        high-water). `wrong_state`: this VERB is illegal from the current state but
        the draft is still live; NEVER retry the same verb; `errors[]` repeats an
        `allowed_action` entry per verb that IS legal right now. `terminal`: the
        review is already sent/auto_sent/cancelled; nothing will ever succeed, stop
        retrying, and a `front_run_next` review event carries the outcome.
        `send_needs_reconciliation`: a prior attempt is unconfirmed and parked for
        recover-by-Message-ID; do NOT resend, poll the review.
      content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } }
    Unavailable:
      description: >
        A dependency could not be read, so the request was failed CLOSED rather than
        served on a guess (Problem `code` = `unavailable`). Retryable; see
        `Retry-After`. Distinct from `not_configured`, which is permanent for this
        deployment.
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds to wait before retrying.
      content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } }
    SendUnprocessable:
      description: >
        The send/reply/forward is well-formed but cannot be processed. Always
        problem+json; branch on `code`. `intent_required` (D3): the resolved review
        mode is `review` and no intent summary was supplied; nothing was sent and
        nothing was queued, and `detail` names the field to add. `recipient_suppressed`:
        one or more recipients have a list-unsubscribe opt-out, and `errors[]` names
        ONLY the suppressed addresses; never the suppression scope or origin; so the
        agent can retry without them.
      content: { application/problem+json: { schema: { $ref: "#/components/schemas/Problem" } } }

paths:
  /v1/billing/stripe/webhook:
    post:
      tags: [billing]
      operationId: stripeBillingWebhook
      summary: Receive Stripe billing webhooks
      description: >
        Unauthenticated by bearer token. Stripe authenticates requests with the
        `Stripe-Signature` HMAC header; invalid signatures are rejected before
        event parsing or persistence.
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object }
      responses:
        "200":
          description: accepted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StripeWebhookAck" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "503": { $ref: "#/components/responses/NotConfigured" }

  /v1/unsubscribe/{token}:
    parameters:
      - name: token
        in: path
        required: true
        description: >
          Opaque, stateless AEAD unsubscribe token placed in the
          `List-Unsubscribe` header of an outbound message. Carries tenant/origin
          context plus either the single recipient (one-click tokens) or truncated
          per-recipient digests (multi-recipient tokens); no recipient PII is in the
          URL.
        schema: { type: string }
    get:
      tags: [unsubscribe]
      operationId: unsubscribePage
      summary: Render the unsubscribe confirmation page
      security: []
      description: >
        Public, unauthenticated. Returns a self-contained HTML page with a confirm
        button ("stop emails from {org}"), a narrower "only this sender" option, and
        an address-confirm field for multi-recipient (digest) tokens. This endpoint
        NEVER mutates state (scanner defense); an invalid/expired token yields a
        graceful static page (still 200, never an enumeration oracle).
      responses:
        "200":
          description: The unsubscribe page (or a graceful invalid-link page).
          content:
            text/html:
              schema: { type: string }
        "429": { description: Throttled (per-IP/token rate limit). }
    post:
      tags: [unsubscribe]
      operationId: unsubscribe
      summary: Record an unsubscribe (RFC 8058 one-click or preferences form)
      security: []
      description: >
        Public, unauthenticated. Accepts `application/x-www-form-urlencoded` and
        `multipart/form-data`. A body carrying `List-Unsubscribe=One-Click` is the
        RFC 8058 one-click path (single-recipient tokens only); it is idempotent,
        returns a plain 200 with NO redirects and no cookies. Otherwise it is the
        preferences-page form: a `scope` field (`narrow` selects the "just this
        sender" opt-out; anything else is org-wide) and, for multi-recipient tokens,
        an `email` field validated against the token's digests before suppressing.
        Invalid/expired tokens and unverified addresses return a 200 page, never a
        status-code enumeration oracle.
      requestBody:
        required: false
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                List-Unsubscribe:
                  type: string
                  description: Set to "One-Click" for the RFC 8058 one-click POST.
                scope:
                  type: string
                  description: "\"narrow\" for a just-this-sender opt-out; else org-wide."
                email:
                  type: string
                  description: Visitor-confirmed address (multi-recipient tokens only).
          multipart/form-data:
            schema:
              type: object
              properties:
                List-Unsubscribe: { type: string }
                scope: { type: string }
                email: { type: string }
      responses:
        "200":
          description: Opt-out recorded (or a graceful invalid/unverified page).
          content:
            text/plain:
              schema: { type: string }
            text/html:
              schema: { type: string }
        "400": { description: Malformed request body. }
        "429": { description: Throttled (per-IP/token rate limit). }

  /healthz:
    get:
      tags: [ops]
      operationId: healthz
      summary: Liveness probe
      security: []
      responses:
        "200": { description: ok }

  /readyz:
    get:
      tags: [ops]
      operationId: readyz
      summary: Readiness probe (checks the store)
      security: []
      responses:
        "200": { description: ready }
        "503": { description: not ready }

  /openapi.yaml:
    get:
      tags: [ops]
      operationId: getOpenApiYaml
      summary: This OpenAPI description (YAML)
      security: []
      responses:
        "200":
          description: The canonical OpenAPI 3 spec.
          content:
            application/yaml:
              schema: { type: string }

  /openapi.json:
    get:
      tags: [ops]
      operationId: getOpenApiJson
      summary: This OpenAPI description (JSON)
      security: []
      responses:
        "200":
          description: The OpenAPI 3 spec converted to JSON.
          content:
            application/json:
              schema: { type: object }

  /v1/enroll:
    post:
      tags: [enrollment]
      operationId: enroll
      x-fern-sdk-group-name: [enroll]
      x-fern-sdk-method-name: create
      summary: Redeem an enrollment token for an agent key
      security: []
      description: >
        Validates the token (not revoked/expired/exhausted) and creates a permissioned
        agent key bound to an agent. Idempotent on `agent_handle`. Also accepts a
        client-supplied idempotency key (`Idempotency-Key` header or `client_id`
        body field) to replay the original response on retry.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EnrollRequest" }
      responses:
        "201":
          description: Agent key created (shown once).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EnrollResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }

  /v1/signup-status:
    get:
      tags: [enrollment]
      operationId: getSignupStatus
      summary: Read current free-signup availability
      security: []
      description: Returns the database-backed global free-signup switch. This response is never cacheable.
      responses:
        "200":
          description: Current signup availability.
          headers:
            Cache-Control:
              schema: { type: string, example: no-store }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignupStatus" }
        "503":
          description: Signup availability could not be determined (`signup_gate_unavailable`).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  /v1/agent/sign-up:
    post:
      tags: [enrollment]
      operationId: signUp
      x-fern-sdk-group-name: [agent]
      x-fern-sdk-method-name: signUp
      summary: Self-signup for a free account + first inbox
      security: []
      description: >
        When the operational gate is enabled, provisions (or idempotently re-provisions)
        a free account and a first inbox on `free.extrovertmail.com`,
        returns a limited-permission agent key that expires with the emailed verification
        code, and sends that code to `human_email`. Call `POST /v1/agent/verify` to
        activate the account and atomically exchange the bootstrap key for a
        full-permission key. Before verification, that key may call verification only
        and the account cannot send. The OTP is never returned in the body. Free signup
        is currently disabled in production.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SignUpRequest" }
      responses:
        "201":
          description: Account created; verification code emailed.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SignUpResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403":
          description: Free signup is temporarily paused (`signup_disabled`).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "502": { $ref: "#/components/responses/BadGateway" }
        "503":
          description: Self-signup is not configured or the signup gate is unavailable.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  /v1/agent/verify:
    post:
      tags: [enrollment]
      operationId: verify
      x-fern-sdk-group-name: [agent]
      x-fern-sdk-method-name: verify
      summary: Verify a signup code and upgrade to a full-permission key
      security: [{ AgentKey: ["signup:verify"] }]
      description: >
        Requires the verification-only `signup:verify` key. Atomically revokes it and returns
        a new full-permission key, activates the free account and its
        `free.extrovertmail.com` inbox, and returns copy-ready MCP
        read_messages/get_message/wait_for_email calls on success.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VerifyRequest" }
      responses:
        "200":
          description: Verified; full-permission key issued.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VerifyResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403":
          description: Free signup verification is temporarily paused (`signup_disabled`).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "503":
          description: Signup availability could not be determined (`signup_gate_unavailable`).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }

  /v1/auth/me:
    get:
      tags: [enrollment]
      operationId: whoami
      x-fern-sdk-group-name: [auth]
      x-fern-sdk-method-name: me
      summary: Introspect the principal behind the agent key
      security: [{ AgentKey: [] }]
      description: Requires only a valid key (any scope), so an agent can always read its own authority.
      responses:
        "200":
          description: The verified principal.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WhoAmI" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/inboxes:
    post:
      tags: [inboxes]
      operationId: createInbox
      x-fern-ignore: true
      summary: Create an inbox
      security: [{ AgentKey: ["mailbox:create"] }]
      description: >
        Create an inbox. Accepts a client-supplied idempotency key
        (`Idempotency-Key` header or `client_id` body field): a retry with the same
        key returns the existing inbox; the same key with a different body is a 409.
      parameters:
        - in: query
          name: paid
          schema: { type: boolean }
          description: When the paid-inbox feature flag is on, requests the x402 payment path (HTTP 402).
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateInboxRequest" }
      responses:
        "201":
          description: Created. `password` is shown once.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Inbox" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { description: x402 payment required (feature-flagged). }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [inboxes]
      operationId: listInboxes
      x-fern-ignore: true
      summary: List the agent's inboxes
      security: [{ AgentKey: ["mailbox:read"] }]
      description: Lists inboxes visible to the key. It never returns passwords or portable credentials.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/PageToken"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  inboxes:
                    type: array
                    items: { $ref: "#/components/schemas/Inbox" }
                  next_page: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  # Project-prefixed canonical SDK chain (x.projects.inboxes.*). A broad (org) key
  # narrows to one project here; a project key may use it only for its own project
  # (else 404). The wildcard /v1/projects/-/inboxes (project_id = "-") is the same
  # path template and serves the org-wide list for an org key. The SAME handlers
  # serve these and the bare /v1/inboxes forms; resolveActive fills Active.
  /v1/projects/{project_id}/inboxes:
    parameters:
      - { in: path, name: project_id, required: true, schema: { type: string }, description: "Project id, or `-` for the org-wide wildcard (org keys only)." }
    post:
      tags: [inboxes]
      operationId: createInboxInProject
      summary: Create an inbox in a project
      x-fern-sdk-group-name: [projects, inboxes]
      x-fern-sdk-method-name: create
      security: [{ AgentKey: ["mailbox:create"] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateInboxRequest" }
      responses:
        "201":
          description: Created. `password` is shown once.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Inbox" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
    get:
      tags: [inboxes]
      operationId: listInboxesInProject
      summary: List inboxes in a project (or the org subtree with `-`)
      x-fern-sdk-group-name: [projects, inboxes]
      x-fern-sdk-method-name: list
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/List" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/projects/{project_id}/inboxes/{inbox_id}:
    parameters:
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/InboxID"
    get:
      tags: [inboxes]
      operationId: getInboxInProject
      summary: Get an inbox in a project
      x-fern-sdk-group-name: [projects, inboxes]
      x-fern-sdk-method-name: get
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Inbox" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [inboxes]
      operationId: updateInboxInProject
      summary: Update an inbox in a project
      x-fern-sdk-group-name: [projects, inboxes]
      x-fern-sdk-method-name: update
      security:
        - { AgentKey: ["mailbox:create", "mailbox:read"] }
        - { AgentKey: ["mailbox:quota", "mailbox:read"] }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateInboxRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Inbox" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [inboxes]
      operationId: deleteInboxInProject
      summary: Delete an inbox in a project
      x-fern-sdk-group-name: [projects, inboxes]
      x-fern-sdk-method-name: delete
      security:
        - { AgentKey: ["mailbox:delete"] }
        - { AgentKey: ["mailbox:create"] }
      responses:
        "204": { description: deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/projects/{project_id}/inboxes/{inbox_id}/credentials:
    parameters:
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/InboxID"
    get:
      tags: [inboxes]
      operationId: getInboxCredentialsInProject
      summary: Get IMAP/SMTP settings for an inbox in a project
      x-fern-sdk-group-name: [projects, inboxes]
      x-fern-sdk-method-name: credentials
      security: [{ AgentKey: ["mailbox:credentials"] }]
      description: Paid-plan only. Re-checks ownership before returning portable mail-client credentials.
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InboxCredentials" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/purchase:
    post:
      tags: [inboxes]
      operationId: purchaseInboxes
      x-fern-sdk-group-name: [inboxes]
      x-fern-sdk-method-name: purchase
      summary: x402 paid purchase of one or more inboxes
      security: [{ AgentKey: ["mailbox:create"] }]
      description: >
        Performs the x402 handshake: a request with no `X-PAYMENT` /
        `PAYMENT-SIGNATURE` header returns `402` with payment requirements; a signed
        retry runs verify → settle → provision. Only available when paid inboxes are
        enabled (otherwise `404`).
      parameters:
        - in: header
          name: X-PAYMENT
          required: false
          schema: { type: string }
          description: base64(JSON) of the signed EIP-3009 payload. `PAYMENT-SIGNATURE` is also accepted.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PurchaseRequest" }
      responses:
        "201":
          description: Settled and provisioned.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PurchaseResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "402": { description: Payment required (handshake step 1) or verify/settle failed. }
        "404": { description: Paid purchase is not enabled. }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/inboxes/{inbox_id}:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    get:
      tags: [inboxes]
      operationId: getInbox
      x-fern-ignore: true
      summary: Get an inbox
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Inbox" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [inboxes]
      operationId: updateInbox
      x-fern-ignore: true
      summary: Update an inbox (rename the sender identity / metadata in place)
      security:
        - { AgentKey: ["mailbox:create", "mailbox:read"] }
        - { AgentKey: ["mailbox:quota", "mailbox:read"] }
      description: >
        Update cheap inbox metadata (e.g. the sender display name) without
        delete+recreate. Owner-scoped: a cross-tenant or cross-agent target is a
        404. Returns the updated inbox.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateInboxRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Inbox" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [inboxes]
      operationId: deleteInbox
      x-fern-ignore: true
      summary: Delete an inbox (tears down the mailbox + authenticated sender)
      security:
        - { AgentKey: ["mailbox:delete"] }
        - { AgentKey: ["mailbox:create"] }
      responses:
        "204": { description: deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/inboxes/{inbox_id}/credentials:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    get:
      tags: [inboxes]
      operationId: getInboxCredentials
      x-fern-ignore: true
      summary: Get IMAP/SMTP connection settings + login for an owned mailbox
      security: [{ AgentKey: ["mailbox:credentials"] }]
      description: Paid-plan only. Re-checks ownership and unseals the mailbox password so an agent can configure a real mail client.
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InboxCredentials" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/inboxes/{inbox_id}/send:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    post:
      tags: [messaging]
      operationId: sendMessage
      x-fern-ignore: true
      summary: Send a message from the inbox (authenticated sender)
      description: >
        Send a message.

        The resolved account/inbox review policy is authoritative on every send.
        Omitting fields does not bypass it. Read
        `effective_review_policy` on `GET /v1/inboxes/{inbox_id}` to know which
        branch you are on:

        - `require_review` (the default for every account): a request WITHOUT an
          `intent` is rejected with `422 intent_required`. Nothing is sent or queued.
          A request WITH an intent returns `202 queued_for_review`. Then monitor the
          review until you receive a `sent` or `send_failed` review event.
        - `allow_direct`: a bare request (no mode/intent/category_id) still sends
          immediately and returns the legacy `202` body, now plus `review_id`.
          Supplying an intent or `mode: review` queues it for a human.
        - `auto_send_graduated`: a categorized message that clears the graduation
          gates auto-sends; everything else is queued.

        Contact lists, list-unsubscribe suppression and the billing quota are all
        enforced at SUBMIT time, so a rejected recipient fails fast rather than
        after a human has already approved the draft.
      security: [{ AgentKey: ["mailbox:send"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SendRequest" }
      responses:
        "200":
          description: Sent immediately (policy-permitted direct or graduated path).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SentResult" }
        "202":
          description: Accepted; either queued for review or accepted for delivery.
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/QueuedForReviewResult" }
                  - { $ref: "#/components/schemas/StatusResult" }
        "400": { $ref: "#/components/responses/SendBadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/ReviewConflict" }
        "422": { $ref: "#/components/responses/SendUnprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/inboxes/{inbox_id}/reply:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    post:
      tags: [messaging]
      operationId: replyMessage
      x-fern-ignore: true
      summary: Reply in-thread (recipients + threading derived)
      description: >
        Reply in-thread. Recipients, subject and threading headers are derived from
        the parent at SUBMIT time, so a queued reply shows the human the real
        envelope and the pre-flight screens the real recipients.

        The resolved account/inbox review policy is authoritative on every send.
        Omitting fields does not bypass it. Read
        `effective_review_policy` on `GET /v1/inboxes/{inbox_id}` to know which
        branch you are on:

        - `require_review` (the default for every account): a request WITHOUT an
          `intent` is rejected with `422 intent_required`. Nothing is sent or queued.
          A request WITH an intent returns `202 queued_for_review`. Then monitor the
          review until you receive a `sent` or `send_failed` review event.
        - `allow_direct`: a bare request (no mode/intent/category_id) still sends
          immediately and returns the legacy `202` body, now plus `review_id`.
          Supplying an intent or `mode: review` queues it for a human.
        - `auto_send_graduated`: a categorized message that clears the graduation
          gates auto-sends; everything else is queued.

        Contact lists, list-unsubscribe suppression and the billing quota are all
        enforced at SUBMIT time, so a rejected recipient fails fast rather than
        after a human has already approved the draft.
      security: [{ AgentKey: ["mailbox:send"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReplyRequest" }
      responses:
        "200":
          description: Sent immediately (policy-permitted direct or graduated path).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SentResult" }
        "202":
          description: Accepted; either queued for review or accepted for delivery.
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/QueuedForReviewResult" }
                  - { $ref: "#/components/schemas/SendResult" }
        "400": { $ref: "#/components/responses/SendBadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/ReviewConflict" }
        "422": { $ref: "#/components/responses/SendUnprocessable" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/reviews:
    get:
      tags: [reviews]
      operationId: listReviews
      x-fern-sdk-group-name: [reviews]
      x-fern-sdk-method-name: list
      summary: List review requests (Review Loop, agent plane)
      description: >
        List the customer's review requests so a sending agent can monitor its
        submissions. Filter by state (repeatable / comma-separated), category_id,
        and inbox. Customer-scoped; human-authority actions are console-only.
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - { in: query, name: state, schema: { type: string }, description: "Filter by state (repeatable or comma-separated, e.g. needs_review)." }
        - { in: query, name: category_id, schema: { type: string }, description: Filter by opaque category id (cat_…). }
        - { in: query, name: inbox, schema: { type: string }, description: Filter by composer inbox address. }
        - $ref: "#/components/parameters/Limit"
        - { in: query, name: page, schema: { type: string }, description: Opaque page cursor. }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/reviews/events:
    get:
      tags: [reviews]
      operationId: listReviewEvents
      x-fern-sdk-group-name: [reviews, events]
      x-fern-sdk-method-name: list
      summary: Drain review events (non-blocking)
      description: >
        Non-blocking drain of the next un-acked review nudges for the calling agent,
        in strict-FIFO-per-review seq order (best-effort across reviews), with the
        per-review cursors. The durable nudge queue is the authoritative liveness
        source; webhook/SSE are best-effort fast paths on top of it. Side-effect
        free and idempotent; re-calling returns the same frontier until an ack
        advances the cursor. Customer-scoped.
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - { in: query, name: review_id, schema: { type: string }, description: Restrict the drain to one review's events (rr_…). }
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewEventsResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/reviews/events/wait:
    get:
      tags: [reviews]
      operationId: waitForReviewEvent
      x-fern-sdk-group-name: [reviews, events]
      x-fern-sdk-method-name: getWait
      summary: Long-poll for a review event
      description: >
        Long-poll (~25–55s) that blocks until a new review nudge is available OR the
        deadline elapses, then returns like list_review_events (empty events on
        timeout, never an error). Cancel-safe: a client disconnect returns promptly.
        Customer-scoped.
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - { in: query, name: review_id, schema: { type: string }, description: Restrict the wait to one review's events (rr_…). }
        - { in: query, name: wait_seconds, schema: { type: integer }, description: "Long-poll budget in seconds (default ~30, capped ~55)." }
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewEventsResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/reviews/events/ack:
    post:
      tags: [reviews]
      operationId: ackReviewEvent
      x-fern-sdk-group-name: [reviews, events]
      x-fern-sdk-method-name: ack
      summary: Ack review events (advance cursor)
      description: >
        Advance the agent's per-(agent, review) cursor(s) to the supplied
        through_seq and/or mark broadcast nudges done. Idempotent and monotonic; an
        older seq is a no-op (exactly-once effect, spec §6). Customer-scoped.
      security: [{ AgentKey: ["mailbox:read"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AckReviewEventRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AckReviewEventResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/reviews/{id}:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [reviews]
      operationId: getReview
      x-fern-sdk-group-name: [reviews]
      x-fern-sdk-method-name: get
      summary: Get one review request (current draft + intent + state)
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Review" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/reviews/{id}/turns:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [reviews]
      operationId: getReviewTurns
      x-fern-sdk-group-name: [reviews]
      x-fern-sdk-method-name: getTurns
      summary: Get a review's append-only thread turns
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewTurnPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/reviews/{id}/feedback:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [reviews]
      operationId: getReviewFeedback
      x-fern-sdk-group-name: [reviews]
      x-fern-sdk-method-name: getFeedback
      summary: Get the human's assembled feedback for a review (diff + comments + decision + new rules)
      description: >
        Assemble the human's feedback for a review (M5; spec §5.2/§11): the unified +
        structured diff of the human edit, the human comments / rejection feedback, the
        decision, and the rules born from this review (rule_ ids whose source_review_id
        is this review). Read-only; tenant-scoped (a foreign id 404s). $0 LLM; pure
        assembly over already-stored turns + rules.
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewFeedback" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/reviews/{id}/chat:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [reviews]
      operationId: postReviewChat
      x-fern-sdk-group-name: [reviews]
      x-fern-sdk-method-name: chat
      summary: Post a chat turn on a review's thread (agent question)
      description: >
        Append an agent_question turn to the review's append-only thread (M5; spec
        §3.2/§4.2/§5.2). When the draft is in_review it transitions to chatting (a
        state_change turn is written atomically); a feedback_added nudge is enqueued to
        the OTHER party (the human reviewer, via the journal/console stream) and a
        review.chat event is emitted. Idempotent on the Idempotency-Key (parity v21) so
        a retried POST never doubles the turn. $0 LLM; the agent composes the question.
        Human chat (the console human_comment side-panel) is console-only (D17).
      security: [{ AgentKey: ["mailbox:send"] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PostReviewChatRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Review" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/reviews/{id}/revision:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [reviews]
      operationId: submitRevision
      x-fern-sdk-group-name: [reviews]
      x-fern-sdk-method-name: revision
      summary: Post a new agent draft (redraft) under a parent_revision CAS
      description: >
        Post a NEW agent draft for a review (M5; spec §5.2; D17 "the human always
        wins"). parent_revision must equal the draft's current revision, else 409 STALE
        with NO mutation (a human/inbound actor moved the draft; it becomes a learning
        / front-run signal). On a clean CAS the proposed_* body is re-rendered in place
        (revision++), the draft returns to needs_review, and the reviewer nudge +
        review event fire (all atomic in one WithTx). rules_version_seen is pinned on
        the new draft. $0 LLM; the agent composes the redraft.
      security: [{ AgentKey: ["mailbox:send"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SubmitRevisionRequest" }
      responses:
        "200":
          description: ok (the redrafted review, now needs_review at revision+1)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Review" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/reviews/{id}/cancel:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [reviews]
      operationId: cancelReview
      x-fern-sdk-group-name: [reviews]
      x-fern-sdk-method-name: cancel
      summary: Withdraw a pending review (composing agent)
      description: >
        The composing agent withdraws its own pending review to the terminal cancelled
        state (M5; spec §3.2 "agent withdraws"). Only the composer may cancel its own
        review (a foreign id, or another agent's draft, 404s; no cross-agent existence
        leak). A terminal review 409s (you cannot withdraw a sent message; D17).
      security: [{ AgentKey: ["mailbox:send"] }]
      responses:
        "200":
          description: ok (the review, now cancelled)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Review" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/reviews/{id}/restamp:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [reviews]
      operationId: restampReview
      x-fern-sdk-group-name: [reviews]
      x-fern-sdk-method-name: restamp
      summary: Re-stamp a draft's rules-version without redrafting ($0 escape valve)
      description: >
        The D19/§8 re-stamp-without-redraft escape valve. The agent asserts "I reviewed
        this draft against rules vX and no change is needed", and the server advances the
        draft's composed_* rules-versions to vX WITHOUT a new draft (no revision bump, no
        body change, no nudge); the cheap counterpart to submit_revision. A born-stale
        draft re-stamped to the current version becomes current-enough and releasable on
        the next reconciliation sweep. against_version above the category's current
        rules-version is 400; a terminal/sent draft is 409; a foreign id (or another
        agent's draft) is 404. $0 LLM; the agent judged; we only record.
      security: [{ AgentKey: ["mailbox:send"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RestampReviewRequest" }
      responses:
        "200":
          description: ok (the review with advanced composed_* versions, body unchanged)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Review" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/reviews/{id}/decision-context:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [reviews]
      operationId: getReviewDecisionContext
      x-fern-sdk-group-name: [reviews]
      x-fern-sdk-method-name: getDecisionContext
      summary: Get the reviewer's decision context for a review (BYO review-agent plane)
      description: >
        The reviewer's read-only decision surface (D5; §9): the intent + current draft +
        the append-only thread + the two-circuit-breaker budget (hop_count vs max_hops,
        and the hard review_deadline). The reviewer is an AGENT granted review:act,
        authorized for THIS review ONLY via a matching ACTIVE review_link (per-inbox beats
        account-wide); review:act alone is necessary, not sufficient. force_to_human is
        true when EITHER breaker has tripped; the reviewer's next reject would be FORCED
        to the human regardless of intent (the human is the only terminal authority, D17).
        A cross-tenant id is 404 (no existence leak); a non-reviewer is 403. No mutation,
        no LLM.
      security: [{ AgentKey: ["review:act"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewDecisionContext" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/reviews/{id}/decision:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [reviews]
      operationId: reviewerDecide
      x-fern-sdk-group-name: [reviews]
      x-fern-sdk-method-name: decision
      summary: Submit a reviewer decision (approve/edit/reject/escalate)
      description: >
        The reviewer's decision on a draft it holds (D5; §9). approve/edit → the PLATFORM
        performs the authenticated send using the COMPOSER's inbox credentials (the reviewer holds
        review:act, NEVER mailbox:send on an inbox it doesn't own; the credential
        boundary); send_path becomes reviewer_approved and terminal success is written
        post-delivery only. reject → the draft returns to the composer (needs_review) and
        hop_count is incremented. escalate → the draft goes to the human queue with an
        escalation_reason. revision + version are the optimistic CAS; a mismatch is 409
        STALE with NO mutation (the human/composer moved the draft; the human always wins,
        D17). The two circuit breakers (hop_count ≥ max_hops, or the hard review_deadline)
        FORCE a reject to the human regardless of intent; forced_by_breaker names the
        tripped breaker. An unknown action is 400; a cross-tenant id is 404; a non-reviewer
        is 403. $0 LLM; the reviewer agent judged; we route, send, and enforce the
        breakers.
      security: [{ AgentKey: ["review:act"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReviewerDecisionRequest" }
      responses:
        "200":
          description: ok (sent via the composer's creds, or returned to the human queue)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewerDecisionResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/categories:
    get:
      tags: [categories]
      operationId: listCategories
      x-fern-sdk-group-name: [categories]
      x-fern-sdk-method-name: list
      summary: Browse the category registry (Review Loop, agent plane)
      description: >
        List the customer's categories (id + name + description + scope + state) so a
        sending agent can fuzzy-match an existing one before composing. The optional
        `match` is a pure lexical/substring filter (NO LLM on our side); the agent
        does the semantic matching. Categories are CUSTOMER-scoped and agent-
        attributed (the deliberate cross-agent-404 exception); identity is opaque
        cat_ ids (D10).
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - { in: query, name: match, schema: { type: string }, description: "Lexical substring filter over name+description+slug (every token must match; NO LLM)." }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CategoryPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [categories]
      operationId: proposeCategory
      x-fern-sdk-group-name: [categories]
      x-fern-sdk-method-name: create
      summary: Propose a new category (agent plane)
      description: >
        Create a category on the fly (D9). It stands IMMEDIATELY (author_kind=agent,
        state supervised) and writes a create audit/undo row. Returns the cat_…;
        match the registry first so you do not duplicate an existing bucket.
      security: [{ AgentKey: ["mailbox:read"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProposeCategoryRequest" }
      responses:
        "201":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/categories/{id}:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [categories]
      operationId: getCategory
      x-fern-sdk-group-name: [categories]
      x-fern-sdk-method-name: get
      summary: Get one category (agent plane)
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [categories]
      operationId: updateCategory
      x-fern-sdk-group-name: [categories]
      x-fern-sdk-method-name: update
      summary: Rename / re-describe a category (agent plane, metadata only)
      description: >
        Update a category's name and/or description; metadata ONLY (D10). Renaming
        never breaks a reference because nothing keys on the name. Any agent in the
        customer may edit (the shared-registry exception); a rename/redescribe undo
        row is written (the audit/undo safety net, D11).
      security: [{ AgentKey: ["mailbox:read"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateCategoryRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/risk-dial:
    get:
      tags: [categories]
      operationId: getRiskDial
      x-fern-sdk-group-name: [riskDial]
      x-fern-sdk-method-name: get
      summary: Read the effective risk dial (Review Loop, agent plane)
      description: >
        Return the effective brand-risk dial: the account-wide default plus every
        category's per-category overrides, each with its resolved effective value.
        The override applies over the account default; null means inherit (D12).
        Read-only; agents may read but NEVER flip the dial (D16/D6); setting the dial
        is a console (human) action. Customer-scoped.
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RiskDial" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/categories/{id}/graduation-status:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [categories]
      operationId: getGraduationStatus
      x-fern-sdk-group-name: [categories]
      x-fern-sdk-method-name: getGraduationStatus
      summary: Read a category's graduation gate status (agent plane)
      description: >
        Report the gates a category has passed / still needs toward the NEXT rung
        (supervised→auto_notify→auto_silent): clean approvals (N / needed), category
        age, the maturity gate (D16, required for auto_silent), the drift counter vs K,
        and whether a human graduate would succeed right now (can_graduate). Read-only;
        an agent proposes graduation via graduation-request but never flips the bit.
        A foreign id is 404.
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GraduationStatus" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/categories/{id}/graduation-request:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [categories]
      operationId: proposeGraduation
      x-fern-sdk-group-name: [categories]
      x-fern-sdk-method-name: graduationRequest
      summary: Propose graduating a category (agent plane; cannot flip the bit)
      description: >
        RECORD an agent's request to graduate this category (D16/D6). It writes a
        durable, NON-MUTATING request transition (evidence for the human to review)
        and returns the current graduation gate status. It does NOT change the
        category state; flipping the graduation bit is a human (console) action. A
        never_graduate category records the request but stays locked. A foreign id is
        404.
      security: [{ AgentKey: ["mailbox:read"] }]
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProposeGraduationRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GraduationStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/categories/{id}/backlog-status:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [categories]
      operationId: getScanBacklogStatus
      x-fern-sdk-group-name: [categories]
      x-fern-sdk-method-name: getBacklogStatus
      summary: Read the D19 backlog-reconciliation status (agent plane)
      description: >
        Read a $0-LLM, non-mutating snapshot of the category's QUEUED drafts reconciled
        by rules-version (D19/§8): how many are stale vs current-enough against the
        current category rules-version + house-style version (a pure integer compare).
        Read-only; an agent READS the picture; the human (console scan-backlog) or the
        graduate/rule-change hooks TRIGGER the actual reconciliation sweep. A foreign id
        is 404.
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ScanBacklogStatus" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/categories/{id}/pacing-state:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [categories]
      operationId: getCategoryPacingState
      x-fern-sdk-group-name: [categories]
      x-fern-sdk-method-name: getPacingState
      summary: Read the demand-driven pacing state for a category (agent plane)
      description: >
        Read a $0-LLM, non-mutating snapshot of the category's demand-driven pacing
        (M7 Slice B/§8): the human review cursor position, the effective window/ceiling/
        interval, the queued count, and each queued draft's in-window/redrafting/
        behind-cursor classification. Read-only; the cursor advances from the human's
        console approve/reject/edit actions. A foreign id is 404.
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CategoryPacingState" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/reputation:
    get:
      tags: [reputation]
      operationId: getReputation
      x-fern-sdk-group-name: [reputation]
      x-fern-sdk-method-name: get
      summary: The caller's org deliverability rollup
      description: >
        Read-only deliverability status for the caller's own org: the derived status
        badge, per-provider/tenant sending status, the latest Sends/Bounces/Complaints
        window, and the open-finding count. Advisor findings degrade to
        `unavailable_vdm_disabled` when VDM is off. Strictly org-scoped.
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: The org reputation rollup.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReputationRollup" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/reputation/findings:
    get:
      tags: [reputation]
      operationId: listDeliverabilityFindings
      x-fern-sdk-group-name: [reputation]
      x-fern-sdk-method-name: listFindings
      summary: List the org's deliverability findings
      description: >
        The caller's org deliverability findings, newest-first. Supports ?status=,
        ?severity=, ?domain=, ?sender= filters and limit/cursor paging. Read-only;
        strictly org-scoped.
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - { in: query, name: status, schema: { type: string, enum: [open, resolved] } }
        - { in: query, name: severity, schema: { type: string, enum: [low, high, unknown] } }
        - { in: query, name: domain, schema: { type: string } }
        - { in: query, name: sender, schema: { type: string } }
        - $ref: "#/components/parameters/Limit"
        - { in: query, name: cursor, schema: { type: string }, description: "Opaque list pagination cursor." }
      responses:
        "200":
          description: A page of deliverability findings.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReputationFindingsPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/suppressions:
    get:
      tags: [suppressions]
      operationId: listSuppressions
      x-fern-sdk-group-name: [suppressions]
      x-fern-sdk-method-name: list
      summary: Pre-check a recipient or list the org's suppression rows
      description: >
        With ?recipient= this is the read-only PRE-CHECK: does the CALLER'S OWN ORG
        suppress this recipient, and which org rows match (returns SuppressionPrecheck).
        Without ?recipient= it is the paged list of the caller's own org suppression
        rows (returns SuppressionPage). Both are strictly scoped to the key's customer:
        global/shared_domain/cross-tenant rows are NEVER returned (R4 non-leakage).
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - { in: query, name: recipient, schema: { type: string }, description: "Present ⇒ pre-check that address; absent ⇒ list. Canonicalized server-side." }
        - { in: query, name: scope, schema: { type: string, enum: [org] }, description: "Optional list filter (only 'org' rows are visible here)." }
        - { in: query, name: include_revoked, schema: { type: boolean }, description: "List: include soft-revoked rows (default active-only)." }
        - $ref: "#/components/parameters/Limit"
        - { in: query, name: cursor, schema: { type: string }, description: "Opaque list pagination cursor." }
      responses:
        "200":
          description: A SuppressionPrecheck (when ?recipient= is set) or a SuppressionPage.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/SuppressionPage"
                  - $ref: "#/components/schemas/SuppressionPrecheck"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/suppressions/{id}/revoke:
    post:
      tags: [suppressions]
      operationId: revokeSuppression
      x-fern-sdk-group-name: [suppressions]
      x-fern-sdk-method-name: revoke
      summary: Revoke (un-suppress) one org-scope suppression row
      description: >
        Owner-decided revoke (plan §7): an agent may revoke ONLY an org-scope row of its
        OWN org. The reason is required and audit-logged. A foreign/global/shared id is a
        404 (no cross-tenant existence oracle); an empty reason is a 400.
      security: [{ AgentKey: ["mailbox:send"] }]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RevokeSuppressionRequest" }
      responses:
        "200":
          description: The revoked row.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Suppression" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/rules:
    get:
      tags: [rules]
      operationId: getRules
      x-fern-sdk-group-name: [rules]
      x-fern-sdk-method-name: list
      summary: Get the ordered writing-rule set (precedence ladder applied)
      description: >
        Return the ORDERED active rule list with the deterministic §7 precedence
        ladder applied SERVER-SIDE (NO LLM on our side): hard before soft; specificity
        per-agent > category > general/house-style; human before agent; newest
        rev/created_at; higher priority. Includes the general/house-style layer (D2)
        IN ADDITION to the named category's rules, plus a soft return cap (≈25/category
        + 25 general, dropping the least-specific general first). The agent reconciles
        the ordered list semantically; we never apply a rule. Rules are CUSTOMER-scoped
        and agent-attributed (the cross-agent exception).
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - { in: query, name: category_id, schema: { type: string }, description: "Category id (cat_…). Empty returns ONLY the house-style/general layer." }
        - { in: query, name: scope, schema: { type: string, enum: [general, category] }, description: "Narrow to one layer (general | category). Default returns both." }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RulePage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    put:
      tags: [rules]
      operationId: saveRule
      x-fern-sdk-group-name: [rules]
      x-fern-sdk-method-name: update
      summary: Save a writing rule (append-only by supersession)
      description: >
        Write or edit a rule (D11; ANY agent may write shared/house-style rules; the
        audit/undo log is the safety net). scope='general' iff category_id is empty
        (house-style, D2). With supersedes_id the write is an EDIT: a new rev (same
        lineage) is inserted and the prior is flipped to superseded; else a new lineage
        is created. Writes a create/supersede audit row, bumps the rule high-water, and
        emits rule_changed nudges to affected in-flight drafts (born-stale, never
        silently invalidated).
      security: [{ AgentKey: ["mailbox:read"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SaveRuleRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Rule" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/rules/audit:
    get:
      tags: [rules]
      operationId: getRuleAudit
      x-fern-sdk-group-name: [rules, audit]
      x-fern-sdk-method-name: list
      summary: Read the rule/category change audit log (agent plane, read-only)
      description: >
        The append-only change/undo audit log spanning rules AND categories (D11),
        optionally narrowed to one entity. Read-only and agent-visible (the audit log
        is the shared safety net). Each row carries the before/after snapshot enabling
        an undo via POST /v1/rules/audit/{udo_id}/undo.
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - { in: query, name: entity_kind, schema: { type: string, enum: [rule, category] }, description: "Narrow to one entity kind." }
        - { in: query, name: entity_id, schema: { type: string }, description: "Narrow to one entity (rule_… or cat_…)." }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RuleAuditPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/rules/audit/{udo_id}/undo:
    parameters:
      - { in: path, name: udo_id, required: true, schema: { type: string } }
    post:
      tags: [rules]
      operationId: undoRuleChange
      x-fern-sdk-group-name: [rules, audit]
      x-fern-sdk-method-name: undo
      summary: Undo a rule change (restore the prior version)
      description: >
        Restore the prior version recorded in an audit row (D11; agents may undo too).
        UNDO = a NEW forward supersession that re-inserts the before snapshot as the
        new active head (action='restore'); the source audit row is marked undone.
        Idempotent: a re-undo of an already-undone row is a clean 409. Safe under
        concurrent edits (the restore CASes on the lineage head). The udo_id is an
        AUDIT row id, hence the nesting under the audit subtree.
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Rule" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/rules/{id}/promote:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [rules]
      operationId: promoteRule
      x-fern-sdk-group-name: [rules]
      x-fern-sdk-method-name: promote
      summary: Promote a rule between the category and general/house-style layers
      description: >
        Move a rule between the category and general/house-style layers via a
        supersession (a new rev with the changed scope). Writes a supersede audit row
        and re-checks affected in-flight drafts. Promoting to category requires the
        rule to already carry a category.
      security: [{ AgentKey: ["mailbox:read"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PromoteRuleRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Rule" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/rules/{id}/retire:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [rules]
      operationId: retireRule
      x-fern-sdk-group-name: [rules]
      x-fern-sdk-method-name: retire
      summary: Retire a rule (soft delete)
      description: >
        Soft-delete a rule (status='retired') via a supersession-style flip; there is
        NO hard delete; the history survives as training data. Writes a retire audit
        row and re-checks affected in-flight drafts.
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Rule" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/inboxes/{inbox_id}/messages:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    get:
      tags: [messaging]
      operationId: listMessages
      x-fern-ignore: true
      summary: List messages in an inbox
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - $ref: "#/components/parameters/Folder"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
        - { in: query, name: from, schema: { type: string }, description: Filter on From. }
        - { in: query, name: to, schema: { type: string }, description: Filter on To. }
        - { in: query, name: subject, schema: { type: string }, description: Filter on Subject. }
        - { in: query, name: unread, schema: { type: boolean }, description: Unread-only when "true". }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessagePage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/inboxes/{inbox_id}/messages/search:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    get:
      tags: [messaging]
      operationId: searchMessages
      x-fern-ignore: true
      summary: Full-text search messages in an inbox
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - { in: query, name: q, required: true, schema: { type: string }, description: Search query (required). }
        - $ref: "#/components/parameters/Folder"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MessagePage" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/inboxes/{inbox_id}/messages/batch:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    patch:
      tags: [messaging]
      summary: Batch update messages (mark read/unread and/or move folder)
      operationId: batchUpdateMessages
      x-fern-ignore: true
      security: [{ AgentKey: ["mailbox:read"] }]
      description: >
        Mark read/unread and/or move folder for a list of message ids that all
        belong to this inbox. At least one of `read` / `folder` must be set.
        Partial failures (an id that is malformed or not owned by this inbox) are
        reported in `failed` rather than failing the whole batch. A folder move
        additionally requires the mailbox:create (or :delete) scope.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BatchUpdateMessagesRequest" }
      responses:
        "200":
          description: Per-id outcome.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BatchUpdateResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/inboxes/{inbox_id}/messages/{id}:
    parameters:
      - $ref: "#/components/parameters/InboxID"
      - $ref: "#/components/parameters/MessageID"
    patch:
      tags: [messaging]
      operationId: markMessage
      x-fern-ignore: true
      summary: Mark a message read or unread
      security: [{ AgentKey: ["mailbox:read"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MarkMessageRequest" }
      responses:
        "200":
          description: The updated message.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [messaging]
      summary: Delete a message (move to Trash, or expunge)
      operationId: deleteMessage
      x-fern-ignore: true
      security:
        - AgentKey: ["mailbox:read", "mailbox:delete"]
        - AgentKey: ["mailbox:read", "mailbox:create"]
      description: >
        Move a message to the Trash folder (a recoverable soft delete). Pass
        `?expunge=true` to permanently remove it instead. A message already in
        Trash is always expunged. Requires mailbox:read plus mailbox:create (or
        mailbox:delete); the owning agent's default key carries both capabilities.
      parameters:
        - { in: query, name: expunge, schema: { type: boolean }, description: Permanently remove instead of moving to Trash. }
      responses:
        "200":
          description: Delete outcome.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeleteResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/inboxes/{inbox_id}/messages/{id}/raw:
    parameters:
      - $ref: "#/components/parameters/InboxID"
      - $ref: "#/components/parameters/MessageID"
    get:
      tags: [messaging]
      operationId: getRawMessage
      x-fern-ignore: true
      summary: Download the raw RFC822 (.eml) bytes of a message
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: The full message source.
          content:
            message/rfc822:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/inboxes/{inbox_id}/messages/{id}/forward:
    parameters:
      - $ref: "#/components/parameters/InboxID"
      - $ref: "#/components/parameters/MessageID"
    post:
      tags: [messaging]
      operationId: forwardMessage
      x-fern-ignore: true
      summary: Forward a message to new recipients
      description: >
        Forward an existing message to new recipients. The subject and the quoted
        parent are materialized at SUBMIT time, so a queued forward shows the human
        the exact forwarded content and an approved forward delivers the reviewer's
        edit rather than a body re-derived from the live parent. A forward carries no
        `In-Reply-To`: its recipients were never part of the parent conversation.

        The resolved account/inbox review policy is authoritative on every send.
        Omitting fields does not bypass it. Read
        `effective_review_policy` on `GET /v1/inboxes/{inbox_id}` to know which
        branch you are on:

        - `require_review` (the default for every account): a request WITHOUT an
          `intent` is rejected with `422 intent_required`. Nothing is sent or queued.
          A request WITH an intent returns `202 queued_for_review`. Then monitor the
          review until you receive a `sent` or `send_failed` review event.
        - `allow_direct`: a bare request (no mode/intent/category_id) still sends
          immediately and returns the legacy `202` body, now plus `review_id`.
          Supplying an intent or `mode: review` queues it for a human.
        - `auto_send_graduated`: a categorized message that clears the graduation
          gates auto-sends; everything else is queued.

        Contact lists, list-unsubscribe suppression and the billing quota are all
        enforced at SUBMIT time, so a rejected recipient fails fast rather than
        after a human has already approved the draft.

        Forward uses the same review policy as send. Any other behavior would create a
        bypass, especially because it
        exfiltrates a received thread.
      security: [{ AgentKey: ["mailbox:send"] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ForwardRequest" }
      responses:
        "200":
          description: Sent immediately (policy-permitted direct or graduated path).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SentResult" }
        "202":
          description: Accepted; either queued for review or accepted for delivery.
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/QueuedForReviewResult" }
                  - { $ref: "#/components/schemas/SendResult" }
        "400": { $ref: "#/components/responses/SendBadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/ReviewConflict" }
        "422": { $ref: "#/components/responses/SendUnprocessable" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/inboxes/{inbox_id}/messages/{id}/attachments:
    parameters:
      - $ref: "#/components/parameters/InboxID"
      - $ref: "#/components/parameters/MessageID"
    get:
      tags: [attachments]
      operationId: listAttachments
      x-fern-ignore: true
      summary: List a message's attachment metadata
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttachmentPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/inboxes/{inbox_id}/messages/{id}/attachments/{attId}:
    parameters:
      - $ref: "#/components/parameters/InboxID"
      - $ref: "#/components/parameters/MessageID"
      - $ref: "#/components/parameters/AttachmentID"
    get:
      tags: [attachments]
      operationId: getAttachment
      x-fern-ignore: true
      summary: Download one attachment's bytes
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: The attachment bytes (Content-Type + Content-Disposition set).
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/messages/{id}:
    parameters:
      - $ref: "#/components/parameters/MessageID"
    get:
      tags: [messaging]
      operationId: getMessage
      x-fern-sdk-group-name: [messages]
      x-fern-sdk-method-name: get
      summary: Get one message by id (owning inbox resolved from the id)
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/inboxes/{inbox_id}/threads:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    get:
      tags: [messaging]
      operationId: listThreads
      x-fern-ignore: true
      summary: List conversation threads (newest-first)
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ThreadPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/inboxes/{inbox_id}/threads/search:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    get:
      tags: [messaging]
      operationId: searchThreads
      x-fern-ignore: true
      summary: Full-text search conversation threads
      security: [{ AgentKey: ["mailbox:read"] }]
      parameters:
        - { in: query, name: q, required: true, schema: { type: string }, description: Search query (required). }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ThreadPage" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/inboxes/{inbox_id}/threads/{id}:
    parameters:
      - $ref: "#/components/parameters/InboxID"
      - { in: path, name: id, required: true, schema: { type: string }, description: Thread id. }
    get:
      tags: [messaging]
      operationId: getThread
      x-fern-ignore: true
      summary: Get one thread with its messages
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "200":
          description: The thread and its messages (oldest-first).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ThreadDetail" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "413":
          description: Thread exceeds the 1,000-message detail safety limit.
    delete:
      tags: [messaging]
      summary: Delete a thread (every message; move to Trash or expunge)
      operationId: deleteThread
      x-fern-ignore: true
      security:
        - AgentKey: ["mailbox:read", "mailbox:delete"]
        - AgentKey: ["mailbox:read", "mailbox:create"]
      description: >
        Remove every message in the thread (across all folders), moving them to
        the Trash folder (recoverable). Pass `?expunge=true` to permanently remove
        them. Requires mailbox:read plus mailbox:create (or mailbox:delete).
      parameters:
        - { in: query, name: expunge, schema: { type: boolean }, description: Permanently remove instead of moving to Trash. }
      responses:
        "200":
          description: Delete outcome (count = messages removed).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeleteResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/inboxes/{inbox_id}/wait:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    post:
      tags: [messaging]
      operationId: waitForEmail
      x-fern-ignore: true
      summary: Block until a matching message arrives (wait_for_email)
      security: [{ AgentKey: ["mailbox:read"] }]
      description: >
        The killer primitive. Blocks (IMAP poll) until the next matching message
        arrives in this inbox or the timeout elapses, returning the message plus an
        extracted OTP code / verification link.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WaitRequest" }
      responses:
        "200":
          description: Matched or timed out.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WaitResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/wait:
    post:
      tags: [messaging]
      operationId: waitForEmailLegacy
      x-fern-ignore: true
      summary: wait_for_email (legacy body-addressed form)
      security: [{ AgentKey: ["mailbox:read"] }]
      description: Same as the inbox-scoped wait, but the mailbox is supplied in the `address` field.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WaitRequest" }
      responses:
        "200":
          description: Matched or timed out.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WaitResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/inboxes/{inbox_id}/stream:
    parameters:
      - $ref: "#/components/parameters/InboxID"
      - $ref: "#/components/parameters/LastEventID"
      - $ref: "#/components/parameters/LastEventIDQuery"
    get:
      tags: [messaging]
      operationId: streamInbox
      x-fern-ignore: true
      summary: Stream events for one inbox (Server-Sent Events)
      security: [{ AgentKey: ["mailbox:read"] }]
      description: >
        Watch one owned inbox live instead of polling wait. Returns a
        `text/event-stream` that emits a `message.received` event (and any other
        webhook event types) as each message arrives; sourced from the same
        new-inbound-message detection the webhook delivery engine uses. Each SSE
        frame's `id:` is a monotonic resume token: on reconnect, pass it as
        `Last-Event-ID` (or the `last_event_id` query param) and the server replays
        every event after it. Heartbeat comments keep the connection alive while
        idle. MCP cannot hold an open stream; call this endpoint directly (or use
        the SDK `stream`/`subscribe` helper).
      responses:
        "200":
          description: An open SSE stream of events.
          content:
            text/event-stream:
              schema: { $ref: "#/components/schemas/StreamEvent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/events:
    parameters:
      - $ref: "#/components/parameters/LastEventID"
      - $ref: "#/components/parameters/LastEventIDQuery"
    get:
      tags: [messaging]
      operationId: streamEvents
      x-fern-ignore: true
      summary: Stream events across all of the agent's inboxes (Server-Sent Events)
      security: [{ AgentKey: ["mailbox:read"] }]
      description: >
        Like the inbox-scoped stream, but emits events across EVERY inbox the agent
        owns. Same `text/event-stream` envelope and `Last-Event-ID` resume contract.
        This stream also backs human-in-the-loop (HITL) events.
      responses:
        "200":
          description: An open SSE stream of events.
          content:
            text/event-stream:
              schema: { $ref: "#/components/schemas/StreamEvent" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/inboxes/{inbox_id}/lists:
    parameters:
      - $ref: "#/components/parameters/InboxID"
    post:
      tags: [messaging]
      operationId: addContactListEntry
      x-fern-ignore: true
      summary: Add an allow/block entry to an inbox's contact lists
      security: [{ AgentKey: ["mailbox:read"] }]
      description: >
        Add one allow/block entry (address or domain) to this inbox's contact
        lists. A `block` entry rejects a matching recipient on send; when any
        `allow` entry exists, sends from this inbox are restricted to recipients
        that match one of them (allowlist mode). Account-wide entries (registered
        on any inbox the agent owns) also apply.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AddContactListRequest" }
      responses:
        "201":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContactListEntry" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
    get:
      tags: [messaging]
      operationId: listContactListEntries
      x-fern-ignore: true
      summary: List the contact-list entries governing an inbox
      security: [{ AgentKey: ["mailbox:read"] }]
      description: >
        List the allow/block entries that govern this inbox: the inbox-specific
        entries plus any account-wide entries.
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContactListPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/inboxes/{inbox_id}/lists/{id}:
    parameters:
      - $ref: "#/components/parameters/InboxID"
      - { in: path, name: id, required: true, schema: { type: string } }
    delete:
      tags: [messaging]
      operationId: deleteContactListEntry
      x-fern-ignore: true
      summary: Delete a contact-list entry by id
      security: [{ AgentKey: ["mailbox:read"] }]
      responses:
        "204": { description: deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/webhooks:
    post:
      tags: [webhooks]
      operationId: registerWebhook
      x-fern-sdk-group-name: [webhooks]
      x-fern-sdk-method-name: create
      summary: Register an inbound HMAC-signed webhook
      security:
        - { AgentKey: ["webhook:write"] }
        - { AgentKey: ["mailbox:read"] }
      description: >
        Register an inbound webhook. Accepts a client-supplied idempotency key
        (`Idempotency-Key` header or `client_id` body field): a retry with the same
        key replays the original registration; the same key with a different body
        is a 409. New keys should grant `webhook:write`; `mailbox:read` remains
        accepted for older keys.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RegisterWebhookRequest" }
      responses:
        "201":
          description: Registered. `secret` is shown once.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
    get:
      tags: [webhooks]
      operationId: listWebhooks
      x-fern-sdk-group-name: [webhooks]
      x-fern-sdk-method-name: list
      summary: List the agent's webhooks (secrets redacted)
      security:
        - { AgentKey: ["webhook:write"] }
        - { AgentKey: ["mailbox:read"] }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/webhooks/{id}:
    parameters:
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [webhooks]
      operationId: getWebhook
      x-fern-sdk-group-name: [webhooks]
      x-fern-sdk-method-name: get
      summary: Get one webhook (secret redacted)
      security:
        - { AgentKey: ["webhook:write"] }
        - { AgentKey: ["mailbox:read"] }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [webhooks]
      operationId: updateWebhook
      x-fern-sdk-group-name: [webhooks]
      x-fern-sdk-method-name: update
      summary: Update a webhook (url, events, inbox filter, active)
      security:
        - { AgentKey: ["webhook:write"] }
        - { AgentKey: ["mailbox:read"] }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateWebhookRequest" }
      responses:
        "200":
          description: Updated (secret redacted).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [webhooks]
      operationId: deleteWebhook
      x-fern-sdk-group-name: [webhooks]
      x-fern-sdk-method-name: delete
      summary: Delete a webhook
      security:
        - { AgentKey: ["webhook:write"] }
        - { AgentKey: ["mailbox:read"] }
      responses:
        "204": { description: deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  # --- agent-facing domains plane (privileged; domain:manage scope) ---

  /v1/commerce/domain-quotes:
    post:
      tags: [commerce]
      operationId: quoteDomain
      x-fern-sdk-group-name: [commerce]
      x-fern-sdk-method-name: quoteDomain
      summary: Quote a new domain without purchasing it
      security: [{ AgentKey: ["commerce:request"] }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/DomainQuoteRequest" } } }
      responses:
        "200": { description: Current time-limited quote, content: { application/json: { schema: { $ref: "#/components/schemas/DomainQuote" } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/commerce/requests/domain-purchases:
    post:
      tags: [commerce]
      operationId: requestDomainPurchase
      x-fern-sdk-group-name: [commerce]
      x-fern-sdk-method-name: requestDomainPurchase
      summary: Request a domain purchase without granting approval or spending
      security: [{ AgentKey: ["commerce:request"] }]
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/CreateDomainPurchaseRequest" } } }
      responses:
        "202": { description: Request recorded, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/commerce/requests/plan-changes:
    post:
      tags: [commerce]
      operationId: requestPlanChange
      x-fern-sdk-group-name: [commerce]
      x-fern-sdk-method-name: requestPlanChange
      summary: Request a plan change for human approval
      security: [{ AgentKey: ["commerce:request"] }]
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/CreatePlanChangeRequest" } } }
      responses:
        "202": { description: Request recorded, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/commerce/requests:
    get:
      tags: [commerce]
      operationId: listCommerceRequests
      x-fern-sdk-group-name: [commerce]
      x-fern-sdk-method-name: listRequests
      summary: List this agent's commerce requests
      security: [{ AgentKey: ["commerce:request"] }]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/PageToken"
      responses:
        "200": { description: ok, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequestPage" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/commerce/requests/{request_id}:
    parameters: [{ in: path, name: request_id, required: true, schema: { type: string } }]
    get:
      tags: [commerce]
      operationId: getCommerceRequest
      x-fern-sdk-group-name: [commerce]
      x-fern-sdk-method-name: getRequest
      summary: Get one commerce request and its exact blocker or progress
      security: [{ AgentKey: ["commerce:request"] }]
      responses:
        "200": { description: ok, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/commerce/requests/{request_id}/cancel:
    parameters: [{ in: path, name: request_id, required: true, schema: { type: string } }]
    post:
      tags: [commerce]
      operationId: cancelCommerceRequest
      x-fern-sdk-group-name: [commerce]
      x-fern-sdk-method-name: cancelRequest
      summary: Cancel an uncommitted commerce request
      security: [{ AgentKey: ["commerce:request"] }]
      responses:
        "200": { description: Cancelled, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/domains:
    get:
      tags: [domains]
      operationId: listDomains
      x-fern-sdk-group-name: [domains]
      x-fern-sdk-method-name: list
      summary: List the customer's domains and their status
      security: [{ AgentKey: ["domain:manage"] }]
      description: >
        List the onboarded domains for the agent's customer (newest-first,
        paginated), each with its onboarding mode and verification/DKIM status.
        The customer/tenant is derived from the agent key.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/PageToken"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentDomainList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [domains]
      operationId: onboardDomain
      x-fern-sdk-group-name: [domains]
      x-fern-sdk-method-name: create
      summary: Add a delegated or purchased domain
      security: [{ AgentKey: ["domain:manage"] }]
      description: >
        Add a domain the customer controls. `ns_delegated` is the default: the
        customer publishes the returned nameserver records and Extrovert serves
        the inbox domain's mail zone. The legacy `purchased` intent creates a commerce request and
        cannot charge or register until human or pre-authorized policy approval;
        use `/v1/commerce/requests/domain-purchases` for its canonical response.
        `ns_delegated` needs `domain:manage` only. `scope` chooses the domain's
        visibility (`org` shared by default, or `project` bound to the key's project).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OnboardAgentDomainRequest" }
      responses:
        "201":
          description: Connected; returns the nameserver records for delegated setup.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AgentDomain"
                  - $ref: "#/components/schemas/CommerceRequest"
        "202":
          description: Legacy purchased intent recorded as a commerce request.
          content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /v1/domains/{domain}:
    parameters:
      - $ref: "#/components/parameters/Domain"
    get:
      tags: [domains]
      operationId: getDomain
      x-fern-sdk-group-name: [domains]
      x-fern-sdk-method-name: get
      summary: Get one domain's detail, status, and nameserver records
      security: [{ AgentKey: ["domain:manage"] }]
      description: >
        Fetch one of the customer's domains: its verification/DKIM status and the
        nameserver records to publish for delegated setup (`delegation_ns`).
        Tenant-scoped: another customer's (or an unknown) domain is a 404.
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentDomain" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [domains]
      operationId: offboardDomain
      x-fern-sdk-group-name: [domains]
      x-fern-sdk-method-name: delete
      summary: Offboard (remove) a domain from the customer
      security: [{ AgentKey: ["domain:manage"] }]
      description: >
        Accept an offboard for a domain and enqueue the full teardown job: it reaps
        the live outbound provider sender identities and the outbound_* routing rows,
        then scrubs the DNS zone/records and the domain row. Returns 202 with a job
        id and a poll URL (`status_url`); poll `GET /v1/jobs/{job_id}` until the
        status is terminal (succeeded/failed/cancelled). Idempotent; a repeat DELETE
        dedupes onto the same teardown job. New mailboxes can no longer be created on
        the domain; mailboxes already created are untouched at the mail layer.
      responses:
        "202":
          description: Offboard accepted; poll the returned job for completion.
          content:
            application/json:
              schema:
                type: object
                required: [object, job_id, status, status_url]
                properties:
                  object: { type: string, example: domain_offboard }
                  job_id: { type: string }
                  status: { type: string }
                  status_url: { type: string, example: /v1/jobs/job-… }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/jobs/{job_id}:
    parameters:
      - name: job_id
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [jobs]
      operationId: getJob
      x-fern-sdk-group-name: [jobs]
      x-fern-sdk-method-name: get
      summary: Poll an async job's status
      security: [{ AgentKey: ["domain:manage"] }]
      description: >
        Poll the status of an async job the agent enqueued (currently the
        domain-offboard teardown returned by `DELETE /v1/domains/{domain}`). Status
        is terminal on `succeeded`, `failed`, or `cancelled`. Tenant-scoped: another
        customer's (or an unknown) job id is a 404.
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                type: object
                required: [object, id, type, status, created_at, updated_at]
                properties:
                  object: { type: string, example: job }
                  id: { type: string }
                  type: { type: string }
                  status: { type: string }
                  created_at: { type: string, format: date-time }
                  updated_at: { type: string, format: date-time }
                  finished_at: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/domains/{domain}/verify:
    parameters:
      - $ref: "#/components/parameters/Domain"
    post:
      tags: [domains]
      operationId: verifyDomain
      x-fern-sdk-group-name: [domains]
      x-fern-sdk-method-name: verify
      summary: Trigger or refresh verification for a domain
      security: [{ AgentKey: ["domain:manage"] }]
      description: >
        Trigger/refresh verification for an onboarded domain and return the
        (possibly advanced) detail. For `purchased` domains this re-drives the
        resumable, money-safe buy/verify/connect pipeline; for `ns_delegated` it
        re-reads the current status and returns the nameserver records the customer
        still needs to publish. Idempotent.
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentDomain" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  # --- admin / console plane (Clerk) ---

  # Admin SESSION + ORG/PROJECT/INVITATION plane (Phase 2). clerkAuth is
  # IDENTITY-ONLY; org-addressed routes derive authority from the identity's
  # MEMBERSHIP in the URL's {org_id}, never from a preselected/cookie customer.

  /v1/admin/me:
    get:
      tags: [admin]
      operationId: adminMe
      x-fern-sdk-group-name: [admin]
      x-fern-sdk-method-name: me
      summary: The identity's reachable orgs + projects (landing scope)
      security: [{ ClerkSession: [] }]
      description: >
        Returns the authenticated Clerk identity plus the orgs/projects it can reach
        (derived from membership). The console uses this to render the org/project
        switchers and to revalidate cookie landing hints. IDENTITY-ONLY; no {org_id}
        in the URL.
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminMe" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/admin/orgs:
    get:
      tags: [admin]
      operationId: listOrgs
      x-fern-sdk-group-name: [admin, orgs]
      x-fern-sdk-method-name: list
      summary: List the identity's reachable orgs
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrgList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [admin]
      operationId: createOrg
      x-fern-sdk-group-name: [admin, orgs]
      x-fern-sdk-method-name: create
      summary: Create a child org (admin+ on the parent)
      security: [{ ClerkSession: [] }]
      description: >
        Create a CHILD org under parent_org_id (required). Requires admin+ on the
        parent. The child inherits the root org's tier/limits and is capped by the
        root's max_child_orgs. Client org/customer ids are never accepted.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateOrgRequest" }
      responses:
        "201":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Org" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    patch:
      tags: [admin]
      operationId: patchOrg
      x-fern-sdk-group-name: [admin, orgs]
      x-fern-sdk-method-name: update
      summary: Update an org's display fields (admin)
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PatchOrgRequest" }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Org" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
    delete:
      tags: [admin]
      operationId: archiveOrg
      x-fern-sdk-group-name: [admin, orgs]
      x-fern-sdk-method-name: delete
      summary: Archive (soft-delete) an org (owner)
      security: [{ ClerkSession: [] }]
      description: >
        Soft-deletes (archives) the org. Guarded: an org with active child orgs or
        active projects cannot be archived. The compatibility customers row is never
        hard-deleted.
      responses:
        "200":
          description: Archived.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Org" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/projects:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: listProjects
      x-fern-sdk-group-name: [admin, orgs, projects]
      x-fern-sdk-method-name: list
      summary: List the org's projects (viewer)
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: query, name: include_archived, schema: { type: boolean } }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProjectList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [admin]
      operationId: createProject
      x-fern-sdk-group-name: [admin, orgs, projects]
      x-fern-sdk-method-name: create
      summary: Create a project in the org (admin)
      security: [{ ClerkSession: [] }]
      description: >
        Create a project under the org. The org's max_projects cap is enforced inside
        the same transaction as the create. Client ids are never accepted.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateProjectRequest" }
      responses:
        "201":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Project" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/projects/{project_id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    patch:
      tags: [admin]
      operationId: patchProject
      x-fern-sdk-group-name: [admin, orgs, projects]
      x-fern-sdk-method-name: update
      summary: Update a project's display fields (admin)
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PatchProjectRequest" }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Project" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
    delete:
      tags: [admin]
      operationId: archiveProject
      x-fern-sdk-group-name: [admin, orgs, projects]
      x-fern-sdk-method-name: delete
      summary: Archive (soft-delete) a project (admin)
      security: [{ ClerkSession: [] }]
      description: >
        Soft-deletes (archives) the project. Guarded: the default project can never
        be archived, and a project with active resources cannot be archived.
      responses:
        "200":
          description: Archived.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Project" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/members:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: listMembers
      x-fern-sdk-group-name: [admin, orgs, members]
      x-fern-sdk-method-name: list
      summary: List the org's members (viewer)
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrgMemberList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/members/{user_id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: user_id, required: true, schema: { type: string } }
    patch:
      tags: [admin]
      operationId: updateMemberRole
      x-fern-sdk-group-name: [admin, orgs, members]
      x-fern-sdk-method-name: update
      summary: Change a member's role (admin; role-capped, last-owner guarded)
      security: [{ ClerkSession: [] }]
      description: >
        Changes a member's role. Role-capped to the actor's own effective role;
        demoting the org's LAST owner is refused (409 last_owner).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateMemberRoleRequest" }
      responses:
        "200":
          description: Updated membership.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrgMember" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
    delete:
      tags: [admin]
      operationId: removeMember
      x-fern-sdk-group-name: [admin, orgs, members]
      x-fern-sdk-method-name: delete
      summary: Remove a member from the org (admin; last-owner guarded)
      security: [{ ClerkSession: [] }]
      description: >
        Removes a member. Removing the org's LAST owner is refused (409 last_owner).
      responses:
        "204": { description: Removed. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/invitations:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: createInvitation
      x-fern-sdk-group-name: [admin, orgs, invitations]
      x-fern-sdk-method-name: create
      summary: Invite a member to the org (admin; role-capped)
      security: [{ ClerkSession: [] }]
      description: >
        Creates (or idempotently reuses an existing pending) org invitation. The raw
        token is returned ONCE on a fresh create (201); a reused pending invite
        returns 200 with no token. Role-capped to the inviter's own role.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateInvitationRequest" }
      responses:
        "200":
          description: Existing pending invitation reused (no new token).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrgInvitation" }
        "201":
          description: Created (raw token shown once).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrgInvitation" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/invitations/accept:
    post:
      tags: [admin]
      operationId: acceptInvitation
      x-fern-sdk-group-name: [admin, invitations]
      x-fern-sdk-method-name: accept
      summary: Accept an invitation (identity-only; email-bound)
      security: [{ ClerkSession: [] }]
      description: >
        Accepts an invitation by its raw token. IDENTITY-ONLY (no {org_id}; the token
        carries the org). The accept binds to the authenticated user's VERIFIED Clerk
        email == the normalized invitation email (else 403). Idempotent for the same
        user; a revoked/expired invitation cannot be accepted.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AcceptInvitationRequest" }
      responses:
        "200":
          description: Accepted (membership written).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrgInvitation" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/signup-claims/{claim_id}/accept:
    parameters:
      - { in: path, name: claim_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: acceptSignupClaim
      x-fern-sdk-group-name: [admin, signupClaims]
      x-fern-sdk-method-name: accept
      summary: Claim an agent-created account (identity-only; email-bound)
      security: [{ ClerkSession: [] }]
      description: >
        Accepts a live system-seeded owner claim previously discovered through
        GET /v1/admin/me. IDENTITY-ONLY: the claim id is only a selector and the
        server accepts it only when its normalized email matches the authenticated
        user's VERIFIED Clerk email. No raw invitation token is required or exposed.
      responses:
        "200":
          description: Accepted (owner membership written).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrgInvitation" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/invitations/{invitation_id}/revoke:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: invitation_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: revokeInvitation
      x-fern-sdk-group-name: [admin, orgs, invitations]
      x-fern-sdk-method-name: revoke
      summary: Revoke a pending invitation (admin)
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: Revoked.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrgInvitation" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/enrollment-tokens:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: issueEnrollmentToken
      x-fern-sdk-group-name: [admin, orgs, projects, enrollmentTokens]
      x-fern-sdk-method-name: create
      summary: Issue an enrollment token (secret shown once)
      security: [{ ClerkSession: [] }]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IssueEnrollmentTokenRequest" }
      responses:
        "201":
          description: Created (token shown once).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IssueEnrollmentTokenResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    get:
      tags: [admin]
      operationId: listEnrollmentTokens
      x-fern-sdk-group-name: [admin, orgs, projects, enrollmentTokens]
      x-fern-sdk-method-name: list
      summary: List enrollment tokens
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: query, name: include_revoked, schema: { type: boolean } }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/PageToken"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EnrollmentTokenList" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/enrollment-tokens/{id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    delete:
      tags: [admin]
      operationId: revokeEnrollmentToken
      x-fern-sdk-group-name: [admin, orgs, projects, enrollmentTokens]
      x-fern-sdk-method-name: delete
      summary: Revoke an enrollment token
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      responses:
        "204": { description: revoked }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/agent-keys:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: issueAgentKey
      x-fern-sdk-group-name: [admin, orgs, projects, agentKeys]
      x-fern-sdk-method-name: create
      summary: Issue an agent API key (secret shown once)
      security: [{ ClerkSession: [] }]
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IssueAgentKeyRequest" }
      responses:
        "201":
          description: Created (key shown once).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IssueAgentKeyResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    get:
      tags: [admin]
      operationId: listAgentKeys
      x-fern-sdk-group-name: [admin, orgs, projects, agentKeys]
      x-fern-sdk-method-name: list
      summary: List issued agent API keys
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: query, name: agent_id, schema: { type: string } }
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/PageToken"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentKeyList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/agent-keys/{id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    delete:
      tags: [admin]
      operationId: revokeAgentKey
      x-fern-sdk-group-name: [admin, orgs, projects, agentKeys]
      x-fern-sdk-method-name: delete
      summary: Revoke an agent API key
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      responses:
        "204": { description: revoked }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/agents:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: listAgents
      x-fern-sdk-group-name: [admin, orgs, projects, agents]
      x-fern-sdk-method-name: list
      summary: List agents
      security: [{ ClerkSession: [] }]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/PageToken"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [admin]
      operationId: createAgent
      x-fern-sdk-group-name: [admin, orgs, projects, agents]
      x-fern-sdk-method-name: create
      summary: Create an agent identity without issuing credentials
      description: Creates an active, project-bound agent for console-managed inboxes. API keys can be issued separately.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateAgentRequest" }
      responses:
        "201":
          description: created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentSummary" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/agents/{id}/disable:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: disableAgent
      x-fern-sdk-group-name: [admin, orgs, projects, agents]
      x-fern-sdk-method-name: disable
      summary: Disable an agent (instantly invalidates its keys)
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      responses:
        "200":
          description: disabled
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentStatusResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/agents/{id}/enable:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: enableAgent
      x-fern-sdk-group-name: [admin, orgs, projects, agents]
      x-fern-sdk-method-name: enable
      summary: Re-enable a disabled agent
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: path, name: id, required: true, schema: { type: string } }
      responses:
        "200":
          description: active
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AgentStatusResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/domains:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminOnboardDomain
      x-fern-sdk-group-name: [admin, orgs, domains]
      x-fern-sdk-method-name: create
      summary: Add a delegated or purchased domain
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OnboardDomainRequest" }
      responses:
        "201":
          description: Connected; returns the nameserver records for delegated setup.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OnboardDomainResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    get:
      tags: [admin]
      operationId: adminListDomains
      x-fern-sdk-group-name: [admin, orgs, domains]
      x-fern-sdk-method-name: list
      summary: List onboarded domains
      security: [{ ClerkSession: [] }]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/PageToken"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DomainList" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/admin/orgs/{org_id}/domains/{domain}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: domain, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetDomain
      x-fern-sdk-group-name: [admin, orgs, domains]
      x-fern-sdk-method-name: get
      summary: Get one domain's detail and nameserver records
      description: >
        Read one onboarded domain the caller's org owns, including the DNS
        nameserver records the customer must publish for `ns_delegated` mode.
        The list endpoint omits records to stay lightweight, so the console reads
        this when a domain row is expanded so the delegation reappears after the
        onboarding dialog is dismissed or the page reloaded.
        Tenant-scoped: an unknown domain, or one owned by a different org, is a
        404 (records are never leaked across tenants).
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: The domain detail with delegation_ns inline.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OnboardDomainResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/reputation:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetReputation
      x-fern-sdk-group-name: [admin, orgs, reputation]
      x-fern-sdk-method-name: get
      summary: The org's deliverability rollup (console)
      description: >
        Read-only deliverability rollup for the org (viewer): status badge, per-
        provider/tenant sending status, latest metrics window, open-finding count.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: The org reputation rollup.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReputationRollup" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/reputation/findings:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminListReputationFindings
      x-fern-sdk-group-name: [admin, orgs, reputation]
      x-fern-sdk-method-name: listFindings
      summary: List the org's deliverability findings (console)
      description: >
        The org's deliverability findings, newest-first (viewer). Supports ?status=,
        ?severity=, ?domain=, ?sender= filters and limit/cursor paging.
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: query, name: status, schema: { type: string, enum: [open, resolved] } }
        - { in: query, name: severity, schema: { type: string, enum: [low, high, unknown] } }
        - { in: query, name: domain, schema: { type: string } }
        - { in: query, name: sender, schema: { type: string } }
        - $ref: "#/components/parameters/Limit"
        - { in: query, name: cursor, schema: { type: string } }
      responses:
        "200":
          description: A page of deliverability findings.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReputationFindingsPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/domains/{domain}/outbound-route:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: domain, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetOutboundRoute
      x-fern-sdk-group-name: [admin, orgs, outboundRoute]
      x-fern-sdk-method-name: get
      summary: The domain's current outbound provider route (console)
      description: >
        Read-only view of the domain's assigned outbound provider account and
        route state (viewer). An unassigned domain returns route_state
        "unassigned" (it routes direct by fail-safe). A domain the caller's org
        does not own returns 404.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: The domain's outbound route.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OutboundRouteView" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/admin/orgs/{org_id}/domains/{domain}/outbound-route/retirements:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: domain, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminListOutboundRouteRetirements
      x-fern-sdk-group-name: [admin, orgs, outboundRoute]
      x-fern-sdk-method-name: retirements
      summary: The domain's retiring-sender ledger (console)
      description: >
        Read-only list of the domain's retiring provider stints (viewer); the
        accounts a switch left decommissioning during their overlap window. The
        active route view clears previous_account_id on flip, so this is the only
        HTTP surface that observes a retiring stint. provider_state_json is never
        exposed. A domain the caller's org does not own returns 404.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: The domain's retiring-sender ledger rows.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OutboundRouteRetirementList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/suppressions:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminListSuppressions
      x-fern-sdk-group-name: [admin, orgs, suppressions]
      x-fern-sdk-method-name: list
      summary: List / search the org's suppression rows
      description: >
        The org's recipient opt-out (suppression) rows, newest-first. Only org-scope
        rows are visible; global/shared-domain rows are operator-only. Supports
        ?recipient=, ?scope=, ?include_revoked=true, and limit/cursor paging.
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: query, name: recipient, schema: { type: string }, description: "Filter by (canonicalized) recipient." }
        - { in: query, name: scope, schema: { type: string, enum: [org] } }
        - { in: query, name: include_revoked, schema: { type: boolean } }
        - $ref: "#/components/parameters/Limit"
        - { in: query, name: cursor, schema: { type: string } }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuppressionPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/suppressions/export:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminExportSuppressions
      x-fern-sdk-group-name: [admin, orgs, suppressions]
      x-fern-sdk-method-name: export
      summary: Export the org's suppression rows as CSV
      description: >
        A CSV of the org's suppression rows (viewer). ?include_revoked=true adds
        soft-revoked rows; ?recipient= / ?scope= narrow the export.
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: query, name: recipient, schema: { type: string } }
        - { in: query, name: scope, schema: { type: string, enum: [org] } }
        - { in: query, name: include_revoked, schema: { type: boolean } }
      responses:
        "200":
          description: CSV export.
          content:
            text/csv:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/suppressions/{id}/revoke:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminRevokeSuppression
      x-fern-sdk-group-name: [admin, orgs, suppressions]
      x-fern-sdk-method-name: revoke
      summary: Revoke (un-suppress) one org-scope suppression row
      description: >
        An org admin (role >= admin) revokes ONE org-scope row with a REQUIRED reason
        (audit-logged). A foreign/global/shared id is a 404; an empty reason is a 400.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RevokeSuppressionRequest" }
      responses:
        "200":
          description: The revoked row.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Suppression" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/commerce/requests:
    parameters: [{ in: path, name: org_id, required: true, schema: { type: string } }]
    get:
      tags: [commerce, admin]
      operationId: adminListCommerceRequests
      summary: List commerce approvals for an org
      security: [{ ClerkSession: [] }]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/PageToken"
        - { in: query, name: kind, schema: { type: string } }
        - { in: query, name: state, schema: { type: string } }
      responses:
        "200": { description: ok, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequestPage" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/commerce/requests/domain-purchases:
    parameters: [{ in: path, name: org_id, required: true, schema: { type: string } }]
    post:
      tags: [commerce, admin]
      operationId: adminCreateDomainPurchaseRequest
      summary: Create an exact-price domain request without charging or registering
      security: [{ ClerkSession: [] }]
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreateDomainPurchaseRequest" } } } }
      responses:
        "201": { description: Review request created, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/commerce/requests/plan-changes:
    parameters: [{ in: path, name: org_id, required: true, schema: { type: string } }]
    post:
      tags: [commerce, admin]
      operationId: adminCreatePlanChangeRequest
      summary: Create a reviewable upgrade or downgrade without charging
      security: [{ ClerkSession: [] }]
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreatePlanChangeRequest" } } } }
      responses:
        "201": { description: Review request created, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/commerce/requests/{request_id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: request_id, required: true, schema: { type: string } }
    get:
      tags: [commerce, admin]
      operationId: adminGetCommerceRequest
      summary: Get a commerce approval request
      security: [{ ClerkSession: [] }]
      responses:
        "200": { description: ok, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/commerce/requests/{request_id}/approve:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: request_id, required: true, schema: { type: string } }
    post:
      tags: [commerce, admin]
      operationId: adminApproveCommerceRequest
      summary: Approve once and optionally create bounded future authority
      security: [{ ClerkSession: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DecideCommerceRequest" } } } }
      responses:
        "200": { description: Decision recorded, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/commerce/requests/{request_id}/deny:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: request_id, required: true, schema: { type: string } }
    post:
      tags: [commerce, admin]
      operationId: adminDenyCommerceRequest
      summary: Deny a pending commerce request
      security: [{ ClerkSession: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DecideCommerceRequest" } } } }
      responses:
        "200": { description: Denied, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/commerce/requests/{request_id}/cancel:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: request_id, required: true, schema: { type: string } }
    post:
      tags: [commerce, admin]
      operationId: adminCancelCommerceRequest
      summary: Cancel a commerce request before irreversible work
      security: [{ ClerkSession: [] }]
      responses:
        "200": { description: Cancelled, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/commerce/requests/{request_id}/retry:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: request_id, required: true, schema: { type: string } }
    post:
      tags: [commerce, admin]
      operationId: adminRetryCommerceRequest
      summary: Resume the same idempotent request after a recoverable blocker
      security: [{ ClerkSession: [] }]
      responses:
        "200": { description: Retry accepted, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceRequest" } } } }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/commerce/requests/{request_id}/payment-link:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: request_id, required: true, schema: { type: string } }
    get:
      tags: [commerce, admin]
      operationId: adminGetCommercePaymentLink
      summary: Get Stripe's hosted payment-authentication link for a human
      security: [{ ClerkSession: [] }]
      responses:
        "200": { description: Secure hosted payment link, content: { application/json: { schema: { type: object, properties: { url: { type: string, format: uri }, expires_at: { type: string } } } } } }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/commerce/spend-limits:
    parameters: [{ in: path, name: org_id, required: true, schema: { type: string } }]
    get:
      tags: [commerce, admin]
      operationId: adminListCommerceSpendLimits
      summary: List bounded future domain-purchase authority
      security: [{ ClerkSession: [] }]
      responses:
        "200": { description: ok, content: { application/json: { schema: { type: object, properties: { items: { type: array, items: { $ref: "#/components/schemas/CommerceSpendLimit" } }, total: { type: integer } } } } } }
    post:
      tags: [commerce, admin]
      operationId: adminCreateCommerceSpendLimit
      summary: Create bounded future domain-purchase authority
      security: [{ ClerkSession: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceSpendLimitInput" } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceSpendLimit" } } } }

  /v1/admin/orgs/{org_id}/commerce/spend-limits/{limit_id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: limit_id, required: true, schema: { type: string } }
    put:
      tags: [commerce, admin]
      operationId: adminUpdateCommerceSpendLimit
      summary: Update a spend control with version fencing
      security: [{ ClerkSession: [] }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceSpendLimitInput" } } } }
      responses:
        "200": { description: Updated, content: { application/json: { schema: { $ref: "#/components/schemas/CommerceSpendLimit" } } } }
        "409": { $ref: "#/components/responses/Conflict" }
    delete:
      tags: [commerce, admin]
      operationId: adminDeleteCommerceSpendLimit
      summary: Disable a spend control
      security: [{ ClerkSession: [] }]
      responses:
        "204": { description: Disabled }

  /v1/admin/orgs/{org_id}/billing:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    get:
      tags: [billing]
      operationId: adminGetBilling
      x-fern-sdk-group-name: [admin, orgs, billing]
      x-fern-sdk-method-name: get
      summary: Get org billing state and plan entitlements
      security: [{ ClerkSession: [] }]
      parameters:
        - in: query
          name: month
          required: false
          schema: { type: string, example: "2026-06" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminBilling" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/billing/checkout:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    post:
      tags: [billing]
      operationId: adminCreateBillingCheckout
      x-fern-sdk-group-name: [admin, orgs, billing]
      x-fern-sdk-method-name: createCheckout
      summary: Create a Stripe Checkout Session for an Extrovert paid plan
      security: [{ ClerkSession: [] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateBillingCheckoutRequest" }
      responses:
        "201":
          description: created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreateBillingCheckoutResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "503": { $ref: "#/components/responses/NotConfigured" }

  /v1/admin/orgs/{org_id}/billing/domain-checkout:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    post:
      tags: [billing]
      operationId: adminCreateDomainBillingCheckout
      x-fern-sdk-group-name: [admin, orgs, billing]
      x-fern-sdk-method-name: createDomainCheckout
      summary: Create a Stripe Checkout Session for an annual managed-domain registration
      description: Quotes the requested domain and a configured .com baseline with the registrar, reuses or creates a lookup-keyed annual Stripe Price, then returns a Checkout Session. Registration is queued only after the domain Checkout succeeds.
      security: [{ ClerkSession: [] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateDomainBillingCheckoutRequest" }
      responses:
        "201":
          description: created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreateDomainBillingCheckoutResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "503": { $ref: "#/components/responses/NotConfigured" }

  /v1/admin/orgs/{org_id}/billing/portal:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    post:
      tags: [billing]
      operationId: adminCreateBillingPortal
      x-fern-sdk-group-name: [admin, orgs, billing]
      x-fern-sdk-method-name: createPortal
      summary: Create a Stripe Billing Portal Session for payment recovery and subscription management
      security: [{ ClerkSession: [] }]
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateBillingPortalRequest" }
      responses:
        "201":
          description: created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CreateBillingPortalResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/NotConfigured" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/audit:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: listAuditEvents
      x-fern-sdk-group-name: [admin, orgs, projects, audit]
      x-fern-sdk-method-name: list
      summary: List the org's console audit log
      security: [{ ClerkSession: [] }]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/PageToken"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuditList" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/webhooks:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminRegisterWebhook
      x-fern-sdk-group-name: [admin, orgs, projects, webhooks]
      x-fern-sdk-method-name: create
      summary: Register a project webhook
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AdminRegisterWebhookRequest" }
      responses:
        "201":
          description: Registered. `secret` is shown once.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    get:
      tags: [admin]
      operationId: adminListWebhooks
      x-fern-sdk-group-name: [admin, orgs, projects, webhooks]
      x-fern-sdk-method-name: list
      summary: List project webhooks
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/webhooks/{id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetWebhook
      x-fern-sdk-group-name: [admin, orgs, projects, webhooks]
      x-fern-sdk-method-name: get
      summary: Get one project webhook
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [admin]
      operationId: adminUpdateWebhook
      x-fern-sdk-group-name: [admin, orgs, projects, webhooks]
      x-fern-sdk-method-name: update
      summary: Update a project webhook
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateWebhookRequest" }
      responses:
        "200":
          description: Updated (secret redacted).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [admin]
      operationId: adminDeleteWebhook
      x-fern-sdk-group-name: [admin, orgs, projects, webhooks]
      x-fern-sdk-method-name: delete
      summary: Delete a project webhook
      security: [{ ClerkSession: [] }]
      responses:
        "204": { description: deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/settings:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: getOrgSettings
      x-fern-sdk-group-name: [admin, orgs, settings]
      x-fern-sdk-method-name: list
      summary: Get the org's console settings
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrgSettings" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [admin]
      operationId: updateOrgSettings
      x-fern-sdk-group-name: [admin, orgs, settings]
      x-fern-sdk-method-name: update
      summary: Update a subset of the org's console settings
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PatchOrgSettingsRequest" }
      responses:
        "200":
          description: Updated settings.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrgSettings" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminListInboxes
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes]
      x-fern-sdk-method-name: list
      summary: List every inbox in the org
      security: [{ ClerkSession: [] }]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/PageToken"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminInboxList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [admin]
      operationId: adminCreateInbox
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes]
      x-fern-sdk-method-name: create
      summary: Create an inbox from the console
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AdminCreateInboxRequest" }
      responses:
        "201":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminInbox" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes/{addr}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/Addr"
    patch:
      tags: [admin]
      operationId: adminUpdateInbox
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes]
      x-fern-sdk-method-name: update
      summary: Update an inbox's enforced daily recipient limit
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AdminUpdateInboxRequest" }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminInbox" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [admin]
      operationId: adminDeleteInbox
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes]
      x-fern-sdk-method-name: delete
      summary: Permanently delete an inbox and its authenticated sender
      security: [{ ClerkSession: [] }]
      responses:
        "204": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/threads:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: listSharedInboxThreads
      x-fern-sdk-group-name: [admin, orgs, projects, threads]
      x-fern-sdk-method-name: list
      summary: Shared-inbox feed; recent threads across every org inbox
      security: [{ ClerkSession: [] }]
      description: Uses bounded per-inbox fan-out, tags each thread with its inbox, and merges them newest-first with a stable global keyset cursor.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminUnifiedThreadFeed" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "413": { description: A mailbox or thread exceeds a documented traversal safety limit. }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes/{addr}/messages:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/Addr"
    get:
      tags: [admin]
      operationId: adminListMessages
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes, messages]
      x-fern-sdk-method-name: list
      summary: List recent messages for an org inbox
      security: [{ ClerkSession: [] }]
      parameters:
        - $ref: "#/components/parameters/Folder"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminMessageList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes/{addr}/messages/{id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/Addr"
      - $ref: "#/components/parameters/MessageID"
    get:
      tags: [admin]
      operationId: adminGetMessage
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes, messages]
      x-fern-sdk-method-name: get
      summary: Get a single message (with attachment metadata) for an org inbox
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminMessageDetail" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes/{addr}/messages/{id}/attachments:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/Addr"
      - $ref: "#/components/parameters/MessageID"
    get:
      tags: [admin]
      operationId: adminListAttachments
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes, messages, attachments]
      x-fern-sdk-method-name: list
      summary: List a message's attachment metadata for an org inbox
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttachmentPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes/{addr}/messages/{id}/attachments/{attId}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/Addr"
      - $ref: "#/components/parameters/MessageID"
      - $ref: "#/components/parameters/AttachmentID"
    get:
      tags: [admin]
      operationId: adminGetAttachment
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes, messages, attachments]
      x-fern-sdk-method-name: get
      summary: Download one attachment's bytes for an org inbox
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: The attachment bytes.
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes/{addr}/threads:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/Addr"
    get:
      tags: [admin]
      operationId: adminListInboxThreads
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes, threads]
      x-fern-sdk-method-name: list
      summary: List persistent threads for an org inbox
      security: [{ ClerkSession: [] }]
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminThreadList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes/{addr}/threads/{thread_id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/Addr"
      - { in: path, name: thread_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetInboxThread
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes, threads]
      x-fern-sdk-method-name: get
      summary: Get one persistent thread and its messages
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: Thread messages, oldest-first.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminThreadDetail" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes/{addr}/send:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/Addr"
    post:
      tags: [admin]
      operationId: adminSendMessage
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes]
      x-fern-sdk-method-name: send
      summary: Send a message from an org inbox
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SendRequest" }
      responses:
        "202":
          description: Accepted for delivery.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StatusResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes/{addr}/reply:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - $ref: "#/components/parameters/Addr"
    post:
      tags: [admin]
      operationId: adminReplyMessage
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes]
      x-fern-sdk-method-name: reply
      summary: Thread-aware console reply from an org inbox
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReplyRequest" }
      responses:
        "202":
          description: Accepted for delivery.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SendResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/realtime/ticket:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminMintRealtimeTicket
      x-fern-sdk-group-name: [admin, orgs, projects, realtime]
      x-fern-sdk-method-name: ticket
      summary: Create a single-use ticket for the console realtime stream
      description: >
        Create a short-lived, single-use ticket bound to the Clerk session's customer
        scope, for opening the live Approvals SSE stream
        (`GET /v1/admin/reviews/stream?ticket=…`). A browser EventSource cannot set
        an Authorization header, so this ticket carries the customer scope instead.
        The ticket is consumed on first stream connect and self-expires (~60s).
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: A freshly created realtime ticket.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RealtimeTicket" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/reviews/stream:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: query, name: ticket, required: true, schema: { type: string }, description: "Single-use ticket from POST /v1/admin/realtime/ticket." }
      - $ref: "#/components/parameters/LastEventID"
      - $ref: "#/components/parameters/LastEventIDQuery"
    get:
      tags: [admin]
      operationId: adminStreamReviews
      x-fern-ignore: true
      summary: Live Approvals queue stream (console plane, Server-Sent Events)
      description: >
        The live human Approvals feed: a `text/event-stream` of `review.*` events
        across EVERY inbox the org owns, sourced from the SAME monotonic event
        journal the agent-plane stream tails; so both planes read one event log
        (spec §6). Authenticated by REDEEMING the single-use `ticket` query parameter
        (created via POST /v1/admin/realtime/ticket), because a browser EventSource
        cannot set a bearer header. Each frame's `id:` is a monotonic resume token:
        on reconnect, pass it as `Last-Event-ID` (or the `last_event_id` query
        param) and the server replays every review event after it. The console
        applies a RealtimeEventDeduper keyed on this id so a replay-on-reconnect
        never double-applies. Customer-scoped to the ticket; no cross-tenant leak.
      responses:
        "200":
          description: An open SSE stream of review.* events.
          content:
            text/event-stream:
              schema: { $ref: "#/components/schemas/StreamEvent" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/reviews:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminListReviews
      x-fern-sdk-group-name: [admin, orgs, projects, reviews]
      x-fern-sdk-method-name: list
      summary: Approvals queue (Review Loop, console plane)
      description: >
        The human Approvals queue: pending reviews across ALL the tenant's inboxes,
        newest-first and paginated, each with an intent summary + a body snippet
        (never the full body). Filter by state (repeatable / comma-separated),
        inbox, and category_id. Customer-scoped to the Clerk session; these
        human-authority reads/actions are console-only and never MCP tools (D17).
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: query, name: state, schema: { type: string }, description: "Filter by state (repeatable or comma-separated)." }
        - { in: query, name: inbox, schema: { type: string }, description: Filter by composer inbox address. }
        - { in: query, name: category_id, schema: { type: string }, description: Filter by opaque category id (cat_…). }
        - $ref: "#/components/parameters/Limit"
        - { in: query, name: page, schema: { type: string }, description: Opaque page cursor. }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminReviewPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/reviews/{id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetReview
      x-fern-sdk-group-name: [admin, orgs, projects, reviews]
      x-fern-sdk-method-name: get
      summary: Get one review (intent + current draft + turns + diff)
      description: >
        The full review for the console detail view: intent, the current draft
        (subject/body), the append-only thread turns, and the diff if the draft was
        revised. Customer-scoped; a foreign id is 404 (no cross-tenant leak).
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminReviewDetail" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/reviews/{id}/approve:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminApproveReview
      x-fern-sdk-group-name: [admin, orgs, projects, reviews]
      x-fern-sdk-method-name: approve
      summary: Approve a draft and send it (human authority)
      description: >
        Approve the draft as-is under the revision+version CAS (D17), with the
        staleness check run INSIDE approve (a stale draft 409s; reconfirm first).
        The platform then performs the authenticated send using the COMPOSER's inbox
        credentials (credential boundary) and records terminal success, returning
        the sent message id + thread id. The send is irreversible once returned.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AdminReviewAction" }
      responses:
        "200":
          description: Approved and sent.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminReviewSendResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/reviews/{id}/edit-send:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminEditSendReview
      x-fern-sdk-group-name: [admin, orgs, projects, reviews]
      x-fern-sdk-method-name: editSend
      summary: Edit a draft, capture the diff, and send (human authority)
      description: >
        The human supplies an edited subject/body. The platform captures the
        proposed-vs-sent diff into a system_diff turn (deterministic Go diff, $0
        LLM), approves under the revision+version CAS, then performs the authenticated send of
        the EDITED content via the COMPOSER's inbox and records terminal success.
        Irreversible once returned; a stale draft or CAS race 409s.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AdminReviewAction" }
      responses:
        "200":
          description: Edited, diffed, and sent.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminReviewSendResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/reviews/{id}/reject:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminRejectReview
      x-fern-sdk-group-name: [admin, orgs, projects, reviews]
      x-fern-sdk-method-name: reject
      summary: Reject a draft (human authority)
      description: >
        Reject the draft under the revision+version CAS, with an optional feedback
        comment persisted as the raw rule-birth signal (D7; empty allowed). The
        composing agent may redraft from rejected. Console-only.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AdminReviewAction" }
      responses:
        "200":
          description: Rejected.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminReviewRejectResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/reviews/{id}/chat:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminReviewChat
      x-fern-sdk-group-name: [admin, orgs, projects, reviews]
      x-fern-sdk-method-name: chat
      summary: Post a human comment on a review's chat thread (console side-panel)
      description: >
        The console chat side-panel (M5; spec §5.3, §10): a human_comment turn whose
        OTHER party is the COMPOSING agent; it enqueues a feedback_added durable nudge
        to the composer (the agent wakes to answer/redraft) and emits a review.chat
        event; when the draft is in_review it flips to chatting. CONSOLE-ONLY (Clerk-
        authed); NEVER an MCP tool (D17). $0 LLM; the human writes the comment.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AdminReviewChatRequest" }
      responses:
        "200":
          description: Comment posted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string }
                  review_id: { type: string }
                  state: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminListCategories
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: list
      summary: Browse the category registry (console plane)
      description: >
        List the shared category registry, scoped to the resolved org/project. The
        agent-plane read (GET /v1/categories) is AgentKey-only, so the console reads
        the SAME view here (Clerk session). The optional `match` is a pure
        lexical/substring filter (NO LLM); identity is opaque cat_ ids (D10).
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: query, name: match, schema: { type: string }, description: "Lexical substring filter over name+description+slug (every token must match; NO LLM)." }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CategoryPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [admin]
      operationId: adminCreateCategory
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: create
      summary: Create a category from the console
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AdminCreateCategoryRequest" }
      responses:
        "201":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories/{id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetCategory
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: get
      summary: Get one category (console plane)
      description: >
        Read one category from the shared registry, scoped to the resolved
        org/project. The agent-plane read (GET /v1/categories/{id}) is AgentKey-only,
        so the console reads the SAME view here (Clerk session). The console re-reads
        after a soft-delete to reflect the self-tombstone. A foreign id is 404.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [admin]
      operationId: adminUpdateCategory
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: update
      summary: Rename / re-describe a category (console plane, human authority)
      description: >
        Rename / re-describe a category with author_kind=human (metadata only, D10);
        writes a rename/redescribe audit/undo row. Console-only (never an MCP tool,
        D17). Customer-scoped; a foreign id is 404.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateCategoryRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [admin]
      operationId: adminDeleteCategory
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: delete
      summary: Soft-delete a category (console plane, human authority)
      description: >
        SOFT delete the category (D9 never-hard-delete): the row + its references +
        history survive as training data. The soft-delete is a self-tombstone
        (merged_into = self) so it drops out of the default registry list; an
        audit/undo row (action=retire) records it. Console-only; a foreign id is 404.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: Soft-deleted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeleteCategoryResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories/{id}/merge:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminMergeCategory
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: merge
      summary: Merge a category into another (console plane, human authority)
      description: >
        Merge the path category (the loser) into into_category_id (the survivor),
        repointing all rr_/rule_ references BY cat_ id and setting merged_into on the
        loser in ONE transaction (D9/D10), plus a merge transition + audit/undo row.
        A cross-tenant or unknown into_category_id is 404 (never a cross-customer
        merge); merging into itself is 400. Console-only.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MergeCategoryRequest" }
      responses:
        "200":
          description: Merged.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MergeCategoryResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories/{id}/graduate:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminGraduateCategory
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: graduate
      summary: Graduate a category one rung up (console plane, human authority)
      description: >
        Flip the graduation bit ONE rung up: supervised→auto_notify→auto_silent
        (D16). never_graduate is locked at supervised (409 graduation_locked); a
        non-adjacent target is 400; graduating to auto_silent requires the MATURITY
        GATE (clean_approval_count ≥ N AND category age ≥ min_age_hours) or it is 409
        maturity_gate_unmet. On success it writes a ctr_ transition, emits
        category.graduated, and enqueues recheck_category nudges to pending drafts.
        Console-only (never an MCP tool, D17). A foreign id is 404.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/GraduateCategoryRequest" }
      responses:
        "200":
          description: Graduated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories/{id}/demote:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminDemoteCategory
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: demote
      summary: Demote a category one rung down (console plane, human authority)
      description: >
        Flip the graduation bit ONE rung down (auto_silent→auto_notify→supervised).
        Demoting from supervised is 409 (nowhere below). On success it clears the
        graduated provenance, writes a ctr_ transition (reason regressed), emits
        category.demoted, and enqueues recheck_category nudges. Console-only (never an
        MCP tool, D17). A foreign id is 404.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: Demoted.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories/{id}/graduation-status:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetGraduationStatus
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: getGraduationStatus
      summary: Read a category's graduation gate status (console plane)
      description: >
        Read the gate status toward the next rung (the maturity gate + drift tripwire
        counters that drive the console graduation controls). The agent-plane read
        (GET /v1/categories/{id}/graduation-status) is AgentKey-only, so the console
        reads the SAME view here (Clerk session). Read-only; a foreign id is 404.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GraduationStatus" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories/{id}/backlog-status:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetBacklogStatus
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: getBacklogStatus
      summary: Read a category's backlog reconciliation status (console plane)
      description: >
        Read the $0-LLM, read-only D19 snapshot of how many queued drafts are stale
        vs current-enough against the current rules-version (§8). The agent-plane read
        (GET /v1/categories/{id}/backlog-status) is AgentKey-only, so the console reads
        the SAME view here (Clerk session) for its reconciliation panel; the human
        scan-backlog button TRIGGERS the sweep. Read-only; a foreign id is 404.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ScanBacklogStatus" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories/{id}/pacing-state:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetPacingState
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: getPacingState
      summary: Read a category's demand-driven pacing state (console plane)
      description: >
        Read the demand-driven pacing snapshot: the human review cursor, the effective
        window/ceiling/interval, the queued count, and each draft's in-window /
        redrafting / behind-cursor classification (the "redrafting…" shimmer). The
        agent-plane read (GET /v1/categories/{id}/pacing-state) is AgentKey-only, so the
        console reads the SAME view here (Clerk session). Read-only; a foreign id is 404.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CategoryPacingState" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories/{id}/scan-backlog:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminScanBacklog
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: scanBacklog
      summary: Run the D19 backlog reconciliation sweep on demand (console plane)
      description: >
        Run the D19/§8 on-demand reconciliation sweep for a category: walk every QUEUED
        draft and INTEGER-COMPARE its composed_* rules-versions against the category's
        CURRENT versions (with a tunable staleness tolerance). A current-enough draft is
        released through the M6 submit-time gate (the agent re-submits to deliver; the
        gate is re-run, never bypassed); a stale draft is nudged to redraft against
        current rules and stays queued. The human never hand-touches the backlog and no
        stale draft slips out. Deterministic, $0-LLM. Console-only (never an MCP tool,
        D17). A foreign id is 404.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ScanBacklogRequest" }
      responses:
        "200":
          description: The reconciliation sweep result (per-message outcomes + counts).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReconcileResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories/{id}/risk-dial:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    patch:
      tags: [admin]
      operationId: adminUpdateCategoryRiskDial
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: updateRiskDial
      summary: Set a category's risk-dial overrides (console plane, human authority)
      description: >
        Set / clear the per-category risk-dial OVERRIDES (D12). For each field, a
        non-null value SETS the override; an explicit JSON null RESETS it back to
        inherit (null = account default); an omitted field is unchanged. Console-only
        (never an MCP tool, D17). A foreign id is 404.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CategoryRiskDialRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CategoryRiskDial" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/risk-dial:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetRiskDial
      x-fern-sdk-group-name: [admin, orgs, projects]
      x-fern-sdk-method-name: getRiskDial
      summary: Read the effective risk dial (console plane)
      description: >
        Read the effective brand-risk dial; the account-wide default plus every
        category's per-category overrides (each with its resolved effective value;
        null override = inherit, D12). The agent-plane read (GET /v1/risk-dial) is
        AgentKey-only, so the console reads the SAME view here (Clerk session) to
        render the risk-dial panel. Read-only; setting the dial is the PATCH above.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RiskDial" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [admin]
      operationId: adminUpdateRiskDial
      x-fern-sdk-group-name: [admin, orgs, projects]
      x-fern-sdk-method-name: updateRiskDial
      summary: Set the account-default risk dial (console plane, human authority)
      description: >
        Set the account-wide risk-dial defaults every inheriting category uses (D4):
        min_confidence, first_contact_gate, drift_demote_after (K), canary_rate,
        graduate_min_approvals, graduate_min_age_hours, auto_send_cap_per_day. A nil
        field is unchanged. Console-only (never an MCP tool, D17).
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RiskDialRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AccountRiskDial" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/categories/{id}/never-graduate:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminSetNeverGraduate
      x-fern-sdk-group-name: [admin, orgs, projects, categories]
      x-fern-sdk-method-name: neverGraduate
      summary: Set/clear the never-graduate lock on a category (console plane, human authority)
      description: >
        Pin a category at supervised (locked=true) or release the pin (locked=false;
        D16). Locking a graduated category FIRST demotes it down to supervised (each
        rung writes its own ctr_ transition) so a locked category can never sit in a
        live auto-send state. Clearing the lock leaves the state untouched. Console-only
        (never an MCP tool, D17). A foreign id is 404.
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NeverGraduateRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/review-policy:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetReviewPolicy
      x-fern-sdk-group-name: [admin, orgs, projects]
      x-fern-sdk-method-name: getReviewPolicy
      summary: Read the account-default review policy (console plane)
      description: >
        The authoritative account-default review policy (D6): require_review,
        allow_direct, or auto_send_graduated. require_review always downgrades an
        agent's per-send mode assertion (an agent can never override it). effective is
        the account policy with the safe require_review floor applied. Console-only
        (Clerk-authed); the agent plane only READS the effective policy.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewPolicy" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    patch:
      tags: [admin]
      operationId: adminUpdateReviewPolicy
      x-fern-sdk-group-name: [admin, orgs, projects]
      x-fern-sdk-method-name: updateReviewPolicy
      summary: Set the account-default review policy (console plane, human authority)
      description: >
        Set the account-default review policy (D6). An invalid policy value is 400.
        Console-only (never an MCP tool, D17).
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ReviewPolicyRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewPolicy" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/inboxes/{addr}/review-policy:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: addr, required: true, schema: { type: string } }
    patch:
      tags: [admin]
      operationId: adminUpdateInboxReviewPolicy
      x-fern-sdk-group-name: [admin, orgs, projects, inboxes]
      x-fern-sdk-method-name: updateReviewPolicy
      summary: Set/clear the per-inbox review-policy override (console plane, human authority)
      description: >
        Set the per-inbox review-policy override (a non-null policy) or clear it back
        to inherit the account default (explicit JSON null; D6). A foreign address is
        404; an invalid non-null policy value is 400. Console-only (never an MCP tool,
        D17).
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InboxReviewPolicyRequest" }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InboxReviewPolicy" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/rules:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetRules
      x-fern-sdk-group-name: [admin, orgs, projects, rules]
      x-fern-sdk-method-name: list
      summary: Read the effective rule stack (console plane)
      description: >
        Read the §7 precedence-ordered effective writing-rule stack for the resolved
        org/project (and optional category), applied SERVER-SIDE (deterministic, NO
        LLM). The agent-plane read (GET /v1/rules) is AgentKey-only, so the console
        reads the SAME ordered list here (Clerk session) for its rules panel.
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: query, name: category_id, schema: { type: string }, description: "Category id (cat_…). Empty returns ONLY the house-style/general layer." }
        - { in: query, name: scope, schema: { type: string, enum: [general, category] }, description: "Narrow to one layer (general | category). Default returns both." }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RulePage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/rules/audit:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    get:
      tags: [admin]
      operationId: adminGetRuleAudit
      x-fern-sdk-group-name: [admin, orgs, projects, rules, audit]
      x-fern-sdk-method-name: list
      summary: Read the rule/category change audit log (console plane)
      description: >
        The human-facing change/undo audit log spanning rules AND categories (D11),
        optionally narrowed to one entity. Console-only (Clerk-authed); the safety net
        for the shared/house-style rule governance. Undo a row via
        POST /v1/admin/rules/audit/{udo_id}/undo.
      security: [{ ClerkSession: [] }]
      parameters:
        - { in: query, name: entity_kind, schema: { type: string, enum: [rule, category] }, description: "Narrow to one entity kind." }
        - { in: query, name: entity_id, schema: { type: string }, description: "Narrow to one entity (rule_… or cat_…)." }
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RuleAuditPage" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/rules/audit/{udo_id}/undo:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: udo_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminUndoRuleChange
      x-fern-sdk-group-name: [admin, orgs, projects, rules, audit]
      x-fern-sdk-method-name: undo
      summary: Undo a rule change (console plane, human authority)
      description: >
        Restore the prior version recorded in an audit row with author_kind=human
        (D11). UNDO = a forward 'restore' supersession; the source row is marked
        undone. Idempotent: a re-undo of an already-undone row is a clean 409.
        Console-only.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Rule" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/ReviewConflict" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/review-links:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
    post:
      tags: [admin]
      operationId: adminCreateReviewLink
      x-fern-sdk-group-name: [admin, orgs, projects, reviewLinks]
      x-fern-sdk-method-name: create
      summary: Link a BYO review-agent to an inbox (console plane)
      description: >
        Link a reviewer agent to an inbox (inbox_address empty = account-wide) so
        reviewed drafts on that scope route to the linked review-agent instead of
        straight to the human queue (D5; §9). One link per scope (the UNIQUE scope
        index); a duplicate scope is 409. The reviewer holds review:act, never
        mailbox:send; the platform sends with the COMPOSER's creds. Console-only
        (Clerk-authed); configuring WHO reviews is a human-authority setup decision,
        NEVER an MCP tool (the reviewer DECISION endpoints are agent-plane).
      security: [{ ClerkSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateReviewLinkRequest" }
      responses:
        "201":
          description: created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewLink" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/ReviewConflict" }
    get:
      tags: [admin]
      operationId: adminListReviewLinks
      x-fern-sdk-group-name: [admin, orgs, projects, reviewLinks]
      x-fern-sdk-method-name: list
      summary: List BYO review-agent links (console plane)
      description: >
        The customer's review links (D5). Secrets are elided (has_secret only).
        Console-only (Clerk-authed).
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReviewLinkList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/admin/orgs/{org_id}/projects/{project_id}/review-links/{rvl_id}:
    parameters:
      - { in: path, name: org_id, required: true, schema: { type: string } }
      - { in: path, name: project_id, required: true, schema: { type: string } }
      - { in: path, name: rvl_id, required: true, schema: { type: string } }
    delete:
      tags: [admin]
      operationId: adminDeleteReviewLink
      x-fern-sdk-group-name: [admin, orgs, projects, reviewLinks]
      x-fern-sdk-method-name: delete
      summary: Delete a BYO review-agent link (console plane)
      description: >
        Remove a review link AND sweep every in-flight review currently routed through
        it back to the HUMAN queue; no orphan deliveries (§9): a review held by the
        deleted reviewer must not stay assigned to a reviewer that no longer exists.
        Console-only (Clerk-authed). A foreign id is 404.
      security: [{ ClerkSession: [] }]
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeleteReviewLinkResult" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

security:
  - AgentKey: []
