openapi: 3.1.0
info:
  title: TopTrendsAI API
  version: 0.9.0
  summary: Asynchronous, self-healing web data jobs
  description: |
    Release contract for the implemented TopTrendsAI v1 API. Unknown request fields are
    rejected. Target-secret and cookie-jar values use dedicated write-only APIs,
    remain encrypted at rest, and are resolved only by authenticated worker leases.
    Saved extraction schemas resolve to an immutable published version during
    job admission. Single-value saved CSS/XPath fields may use versioned,
    tenant- and origin-isolated structural adaptive baselines. BYO
    proxy credentials are accepted only while creating or updating their policy,
    encrypted at rest, and never returned. Future operations remain in the product
    roadmap and are added here only when an executable route exists.
  license:
    name: Proprietary
    url: https://toptrends.ai/terms-of-service
servers:
  - url: https://toptrends.ai
    description: Production API on the canonical application origin
  - url: /
    description: Current origin, including local development
security:
  - bearerAuth: []
tags:
  - name: Scrapes
    description: Submit bounded scrape, batch, and same-site crawl jobs.
  - name: Sessions
    description: Manage tenant-scoped, version-fenced persistent browser contexts.
  - name: Schedules
    description: Manage durable, personal-workspace recurring Scrape, Batch, or Crawl admission.
  - name: Jobs
    description: Inspect, stream, and cancel asynchronous work.
  - name: Extraction schemas
    description: Manage tenant-scoped immutable selector definitions and publication pointers.
  - name: Proxy policies
    description: Configure tenant-scoped managed Webshare or BYO proxy selection.
  - name: Secrets
    description: Manage write-only target authorization, action values, and origin-bound cookies.
  - name: Webhooks
    description: Configure verified, HMAC-signed terminal job notifications.
  - name: Artifacts
    description: Access authorized large outputs without exposing storage keys.
  - name: Exports
    description: Generate and download bounded immutable result snapshots.
  - name: Billing
    description: Reconcile commercial contracts, credit packs, paid periods, usage, and credits.
  - name: Account
    description: Same-origin console account lifecycle operations.
  - name: Workspaces
    description: Same-origin console team workspace, invitation, and membership operations.

paths:
  /api/config/public:
    get:
      tags: [Billing]
      operationId: getPublicRuntimeConfig
      summary: Read low-sensitivity public runtime capabilities
      description: |
        Returns public-safe authentication and checkout switches plus the
        server-authoritative credit-recovery sellability capability. Credit
        recovery is available only when a currently effective enabled credit
        pack and an effective checkout provider form at least one combination
        accepted by checkout's exact money-term validation.
        Catalog scheduling, product terms, provider identities, credentials,
        and internal readiness failures are not disclosed by this capability.
      security: []
      responses:
        "200":
          description: Public runtime configuration
          headers:
            Cache-Control:
              schema:
                type: string
              description: No-store runtime capability policy.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicRuntimeConfigEnvelope"
        "400":
          description: Query parameters are not accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentCheckoutErrorEnvelope"

  /api/config/pricing:
    get:
      tags: [Billing]
      operationId: listPublicCommercialProducts
      summary: List currently effective commercial products
      description: |
        Returns the server-authoritative public catalog, including each
        immutable product-version ID and fulfillment kind. Checkout clients
        send both the product-family key and displayed version ID so a later
        catalog release cannot silently change the displayed terms.
      security: []
      responses:
        "200":
          description: Effective public catalog
          headers:
            Cache-Control:
              schema:
                type: string
              description: Public catalog cache policy.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicCommercialProductEnvelope"
        "400":
          description: Query parameters are not accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentCheckoutErrorEnvelope"

  /api/console/v1/admin/commercial-catalog/products/{product_key}/versions:
    post:
      tags: [Billing]
      operationId: createCommercialProductVersion
      summary: Publish an immutable commercial product version
      description: |
        Requires an authenticated same-origin console session with
        admin.commercial_catalog.write. A product family cannot change its
        fulfillment kind across versions. Credit packs must be one-time.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - name: product_key
          in: path
          required: true
          schema:
            type: string
            pattern: "^[a-z][a-z0-9_]{2,63}$"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCommercialProductVersionInput"
      responses:
        "201":
          description: Immutable product version published
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminCommercialProductVersionEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /api/console/v1/account-deletion:
    post:
      tags: [Account]
      operationId: requestAccountDeletion
      summary: Irreversibly revoke access and request account erasure
      description: |
        Requires an authenticated same-origin console session created within
        the previous 15 minutes, an exact
        `DELETE <current email>` confirmation, and an idempotency key.
        Nonterminal or paid-through subscriptions, provider-uncertain/open
        checkouts, and owned team workspaces block the request before any data
        changes. A canceled or expired subscription is released only after all
        paid and grace access has ended. A successful response means credentials,
        sessions, and workspace access have already been revoked; asynchronous
        content cleanup may still be in progress. Managed legacy upload objects
        enter explicit manual review.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Traceparent"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AccountDeletionRequest"
      responses:
        "200":
          description: Existing deletion request replayed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountDeletionEnvelope"
        "202":
          description: Access revoked and deletion lifecycle accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountDeletionEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /api/console/v1/workspaces:
    get:
      tags: [Workspaces]
      operationId: listConsoleWorkspaces
      summary: List the signed-in user's active workspaces
      description: |
        Requires an authenticated console session. Returns active personal and
        team workspaces for which the signed-in user has an active membership.
        The current member role is included in each resource. This bounded v1
        list is not cursor-paginated, so `meta.next_cursor` is always null.
      security:
        - consoleSession: []
      responses:
        "200":
          description: Active accessible workspaces
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspacePage"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Workspaces]
      operationId: createConsoleTeamWorkspace
      summary: Create a team workspace
      description: |
        Requires an authenticated exact same-origin console mutation. A user
        may belong to at most one active team workspace in this release. The
        creator becomes the owner; the user's personal workspace is unchanged.
      security:
        - consoleSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTeamWorkspaceInput"
      responses:
        "201":
          description: Team workspace created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /api/console/v1/workspaces/{workspace_id}:
    get:
      tags: [Workspaces]
      operationId: getConsoleWorkspace
      summary: Read an accessible workspace
      description: |
        Requires an authenticated console session and an active membership in
        the active workspace. Inaccessible and absent workspaces share the same
        404 response.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Accessible workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    patch:
      tags: [Workspaces]
      operationId: renameConsoleTeamWorkspace
      summary: Rename a team workspace
      description: |
        Requires an authenticated exact same-origin console mutation and an
        owner or admin role. `expected_version` prevents a stale rename.
        Personal, inaccessible, and unauthorized workspaces are hidden as 404.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RenameTeamWorkspaceInput"
      responses:
        "200":
          description: Renamed team workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    delete:
      tags: [Workspaces]
      operationId: closeConsoleTeamWorkspace
      summary: Close a team workspace
      description: |
        Requires an authenticated exact same-origin console mutation and the
        owner role. Closing is idempotent after a successful close. It revokes
        pending invitations and workspace-bound API keys, suspends members,
        disables the team's execution limits and nondeleted Webhook endpoints,
        and cancels queued, retrying, claimed, or dead-letter Webhook
        deliveries while clearing their claims. A request already accepted by
        a receiver cannot be recalled, so receivers must deduplicate stable
        event IDs. Nonterminal jobs or active credit reservations return 409
        until they are resolved.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      responses:
        "200":
          description: Closed team workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /api/console/v1/workspaces/{workspace_id}/invitations:
    get:
      tags: [Workspaces]
      operationId: listConsoleWorkspaceInvitations
      summary: List team workspace invitations
      description: |
        Requires an authenticated console owner or admin. Results are newest
        first, filtered by one exact status, and cursor-paginated. Expired
        pending invitations are marked expired before the requested page is
        read. Invitation email addresses and token material are never returned
        by this list.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceListLimit"
        - $ref: "#/components/parameters/WorkspaceListCursor"
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [pending, accepted, revoked, expired]
            default: pending
      responses:
        "200":
          description: Cursor-paginated workspace invitations
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceInvitationPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Workspaces]
      operationId: createConsoleWorkspaceInvitation
      summary: Create a team workspace invitation
      description: |
        Requires an authenticated exact same-origin console mutation. Owners
        may invite admins or members; admins may invite members only. At most
        25 pending invitations and 25 active members are allowed per team.
        Re-inviting the same normalized email revokes the prior pending
        invitation and creates a fresh one, but an admin cannot rotate an
        owner-created pending admin invitation. After the database transaction
        commits, TopTrends awaits one bounded best-effort email attempt using
        fresh strict provider configuration. Email configuration or provider
        failure does not roll back the invitation: every successful create
        returns `201`, the one-time plaintext token, and a closed delivery
        result. No durable invitation-email retry queue exists in v1, so the
        caller must preserve the returned join link or token. Reissuing rotates
        the prior token and makes a new independent delivery attempt.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWorkspaceInvitationInput"
      responses:
        "201":
          description: Invitation created, one-time token returned, and email attempt reported
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedWorkspaceInvitationEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /api/console/v1/workspaces/{workspace_id}/invitations/{invitation_id}:
    delete:
      tags: [Workspaces]
      operationId: revokeConsoleWorkspaceInvitation
      summary: Revoke a pending team workspace invitation
      description: |
        Requires an authenticated exact same-origin console mutation. Owners
        may revoke any pending invitation; admins may revoke member
        invitations only. A previously revoked invitation is returned
        idempotently. Other terminal invitations return 409.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceInvitationId"
      responses:
        "200":
          description: Revoked workspace invitation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceInvitationEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /api/console/v1/workspace-invitations/accept:
    post:
      tags: [Workspaces]
      operationId: acceptConsoleWorkspaceInvitation
      summary: Accept a team workspace invitation
      description: |
        Requires an authenticated exact same-origin console mutation. The
        signed-in user's normalized account email must match the invitation.
        Invalid, expired, revoked, email-mismatched, and unavailable tokens
        share the same 404 response. A user may belong to at most one active
        team workspace. Repeating an already successful acceptance with the
        same user and token returns the existing membership.
      security:
        - consoleSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AcceptWorkspaceInvitationInput"
      responses:
        "200":
          description: Invitation accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceMemberEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /api/console/v1/workspaces/{workspace_id}/members:
    get:
      tags: [Workspaces]
      operationId: listConsoleWorkspaceMembers
      summary: List team workspace members
      description: |
        Requires an active membership in the team workspace. Any active member
        may list the active directory; `status=suspended` requires an owner or
        admin role. Results are newest first, filtered by one exact status, and
        cursor-paginated. Member names and emails are visible only within that
        authorization boundary.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceListLimit"
        - $ref: "#/components/parameters/WorkspaceListCursor"
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [active, suspended]
            default: active
      responses:
        "200":
          description: Cursor-paginated workspace members
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceMemberPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /api/console/v1/workspaces/{workspace_id}/members/{member_id}:
    patch:
      tags: [Workspaces]
      operationId: updateConsoleWorkspaceMemberRole
      summary: Update a team member role or transfer workspace ownership
      description: |
        Requires an authenticated exact same-origin console mutation. Ordinary
        role updates accept only `admin` or `member` plus optimistic
        `expected_version`.

        The closed `owner` branch additionally requires
        `expected_workspace_version`. Only the current active owner may promote
        a different active target. The transaction promotes the target, demotes
        the previous owner to `admin`, and advances both membership versions
        and the workspace version. It returns 409 while the workspace has a
        nonterminal Job or reserved credit, with only
        `details.blocker=nonterminal_jobs` or
        `details.blocker=active_credit_reservations`. A stale target or
        workspace exposes only `details.current_version` or
        `details.current_workspace_version`.

        Newly admitted Team Jobs use the new owner's credits. Existing Jobs
        and reservations retain their frozen admission-time payer. Repeating
        the exact committed transfer with the same actor, target, and original
        version pair returns the promoted target without applying the transfer
        twice while the resulting versions remain current. Other unauthorized
        or inaccessible resources remain hidden.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceMemberId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWorkspaceMemberInput"
      responses:
        "200":
          description: Updated workspace member
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceMemberEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            Member/workspace version conflict, nonterminal Job blocker, or
            active credit-reservation blocker. An ordinary role update against
            the current owner returns
            `details.blocker=ownership_transfer_required`; an inconsistent
            billing-owner projection may return the same status without
            details. Other safe details contain only the applicable
            `current_version`, `current_workspace_version`, or closed
            `blocker`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/PublicApiProblem"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    delete:
      tags: [Workspaces]
      operationId: suspendConsoleWorkspaceMember
      summary: Suspend a team workspace member
      description: |
        Requires an authenticated exact same-origin console mutation. Owners
        may suspend admins or members; admins may suspend members. Suspension
        revokes that user's API keys bound to the team. The owner cannot be
        suspended.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/WorkspaceId"
        - $ref: "#/components/parameters/WorkspaceMemberId"
      responses:
        "200":
          description: Suspended workspace member
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceMemberEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /api/payment/checkout:
    post:
      tags: [Billing]
      operationId: createPaymentCheckout
      summary: Create or replay one server-authoritative payment checkout
      description: |
        Requires an authenticated exact same-origin console request and a
        16–128 character Idempotency-Key. Commercial terms are loaded from the
        effective catalog and frozen before provider I/O, including the closed
        `fulfillment_kind` (`contract` or `credit_pack`). A credit pack must be
        one-time, may coexist with an active subscription or lifetime contract,
        and never changes that contract's slot, entitlement, or limits. All
        fulfillment kinds remain serialized to one open checkout per user. A
        `checkout_in_progress` conflict directs a credit-pack order to
        `/settings/credits` and a contract order to `/settings/billing`. The
        displayed immutable product version must still be effective; otherwise
        the server returns 409 catalog_version_changed instead of silently
        charging newly published terms.
        Controlled failures preserve their status and expose only a safe error
        code and bounded details in the legacy console response envelope.
        Success returns both the checkout URL and the durable, user-owned
        operation identifier used to correlate a later provider return.
      security:
        - consoleSession: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePaymentCheckoutRequest"
      responses:
        "200":
          description: Checkout session created or replayed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentCheckoutEnvelope"
        "400":
          $ref: "#/components/responses/PaymentCheckoutError"
        "401":
          $ref: "#/components/responses/PaymentCheckoutError"
        "403":
          $ref: "#/components/responses/PaymentCheckoutError"
        "404":
          $ref: "#/components/responses/PaymentCheckoutError"
        "409":
          $ref: "#/components/responses/PaymentCheckoutError"
        "413":
          $ref: "#/components/responses/PaymentCheckoutError"
        "415":
          $ref: "#/components/responses/PaymentCheckoutError"
        "429":
          $ref: "#/components/responses/PaymentCheckoutRateLimited"
        "500":
          $ref: "#/components/responses/PaymentCheckoutError"
        "503":
          $ref: "#/components/responses/PaymentCheckoutError"

  /api/payment/checkout/recover:
    get:
      tags: [Billing]
      operationId: discoverPaymentCheckoutRecovery
      summary: Discover the authenticated user's recoverable checkout
      description: |
        Requires an authenticated console session and rejects every query
        parameter. This read-only operation looks up only the exact
        authenticated user's unique open checkout in the local database. It
        never calls a payment provider and returns no provider, price, money,
        product, or catalog metadata.

        Success is always private and non-cacheable. `data` is null when the
        user has no open checkout. Otherwise it is the strict recovery
        projection `{ operation_id, fulfillment_kind, state, can_resume }`.
        The only projected states are `creating_checkout`,
        `awaiting_payment`, and `recovery_required`, and `can_resume` is always
        true. This lets the Credits and Payments screens discover the same
        server-owned operation after a tab, browser, or device change and
        invoke the existing POST recovery operation. A contract recovery still
        requires its personal-workspace slot; a credit-pack recovery never
        acquires or changes that slot. `can_resume: true` does not bypass an
        active POST attempt lease, which continues to return 409 with
        Retry-After.
      security:
        - consoleSession: []
      responses:
        "200":
          description: Recoverable checkout summary or null
          headers:
            Cache-Control:
              $ref: "#/components/headers/PrivateNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentCheckoutRecoveryDiscoveryEnvelope"
        "400":
          $ref: "#/components/responses/PaymentCheckoutRecoveryError"
        "401":
          $ref: "#/components/responses/PaymentCheckoutRecoveryError"
        "403":
          $ref: "#/components/responses/PaymentCheckoutRecoveryError"
        "429":
          $ref: "#/components/responses/PaymentCheckoutRecoveryRateLimited"
        "500":
          $ref: "#/components/responses/PaymentCheckoutRecoveryError"
        "503":
          $ref: "#/components/responses/PaymentCheckoutRecoveryError"
    post:
      tags: [Billing]
      operationId: recoverPaymentCheckout
      summary: Resume the authenticated user's reserved checkout
      description: |
        Requires an authenticated exact same-origin console request. No browser
        idempotency key or commercial terms are accepted. The user's unique open
        checkout identifies the frozen order; an existing URL is replayed or a
        stale/provider-uncertain attempt is reclaimed with the same provider
        identity. A contract order must still own the personal-workspace slot;
        a credit-pack order is recovered without acquiring or changing that
        slot. An active attempt lease returns 409 and Retry-After. Success
        returns the same durable operation identifier as checkout creation.
      security:
        - consoleSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              maxProperties: 0
      responses:
        "200":
          description: Recoverable checkout session returned
          headers:
            Cache-Control:
              $ref: "#/components/headers/PrivateNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentCheckoutEnvelope"
        "401":
          $ref: "#/components/responses/PaymentCheckoutRecoveryError"
        "403":
          $ref: "#/components/responses/PaymentCheckoutRecoveryError"
        "400":
          $ref: "#/components/responses/PaymentCheckoutRecoveryError"
        "409":
          description: No recoverable reservation or another attempt owns the lease
          headers:
            Cache-Control:
              $ref: "#/components/headers/PrivateNoStore"
            Retry-After:
              description: Present when an active checkout attempt lease should be retried
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentCheckoutErrorEnvelope"
        "429":
          $ref: "#/components/responses/PaymentCheckoutRecoveryRateLimited"
        "500":
          $ref: "#/components/responses/PaymentCheckoutRecoveryError"
        "503":
          $ref: "#/components/responses/PaymentCheckoutRecoveryError"

  /api/payment/checkout/status:
    post:
      tags: [Billing]
      operationId: getPaymentCheckoutStatus
      summary: Read one authenticated user's durable checkout status
      description: |
        Requires an authenticated exact same-origin console request and a strict
        JSON body containing only `operation_id`. The operation is looked up
        under the exact authenticated user; an absent or other user's identifier
        receives the same 404 response. This endpoint reads only durable local
        database state and never polls a payment provider.

        Every response is private and non-cacheable. A provider callback may add
        `checkout_return=<operation_id>` to a same-origin return URL, but that
        marker is correlation data, not payment evidence. A root UI observer
        retains the operation and its original absolute deadline across
        same-tab refreshes, route changes, repeated same-operation returns, and
        sign-in. It polls this endpoint every two seconds for at most 90 seconds
        and only while the state is `creating_checkout` or `awaiting_payment`.
        It stops on `recovery_required`, a terminal state, or at the deadline.
        Verified webhook/reconciliation input and the resulting durable database
        state remain authoritative.
      security:
        - consoleSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PaymentCheckoutStatusRequest"
      responses:
        "200":
          description: Durable checkout operation state
          headers:
            Cache-Control:
              $ref: "#/components/headers/PrivateNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentCheckoutStatusEnvelope"
        "400":
          $ref: "#/components/responses/PaymentCheckoutStatusError"
        "401":
          $ref: "#/components/responses/PaymentCheckoutStatusError"
        "403":
          $ref: "#/components/responses/PaymentCheckoutStatusError"
        "404":
          $ref: "#/components/responses/PaymentCheckoutStatusError"
        "413":
          $ref: "#/components/responses/PaymentCheckoutStatusError"
        "415":
          $ref: "#/components/responses/PaymentCheckoutStatusError"
        "429":
          $ref: "#/components/responses/PaymentCheckoutStatusRateLimited"
        "500":
          $ref: "#/components/responses/PaymentCheckoutStatusError"

  /api/user/commercial-contract/current:
    get:
      tags: [Billing]
      operationId: getCurrentCommercialContract
      summary: Read the authenticated user's current commercial contract fence
      description: |
        Returns the personal workspace's authoritative contract slot and access
        projection. A null data value means no projection exists. Responses are
        private and non-cacheable so checkout UI must fail closed when this read
        is unavailable.
      security:
        - consoleSession: []
      responses:
        "200":
          description: Current commercial contract projection or null
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentCommercialContractEnvelope"
        "401":
          $ref: "#/components/responses/PaymentCheckoutError"
        "500":
          $ref: "#/components/responses/PaymentCheckoutError"

  /v1/schedules:
    get:
      tags: [Schedules]
      operationId: listSchedules
      summary: List recurring Scrape, Batch, or Crawl schedules
      description: |
        Returns cursor-paginated Scrape, Batch, and Crawl schedules owned by
        the authenticated personal workspace, including each schedule's latest
        durable run record. Deleted schedules are excluded. Team-bound keys are
        not supported by the schedules surface. This operation never exposes
        internal workspace or actor identifiers.
      x-required-api-key-scope: schedules:read
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: state
          in: query
          required: false
          schema:
            type: string
            enum: [active, paused]
      responses:
        "200":
          description: Cursor-paginated schedules
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SchedulePage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Schedules]
      operationId: createSchedule
      summary: Create a recurring Scrape, Batch, or Crawl schedule
      description: |
        Stores one normalized Scrape, ordered Batch, or Crawl specification
        behind a strict five-field cron expression evaluated in the requested
        IANA timezone. Schedules are personal-workspace-only in this release.
        A schedule does not reserve credits or capacity: every due run is
        admitted through the current workload policy, balance, limits, and
        worker availability. All workload kinds run direct-first. A Scrape
        uses a proxy only when `spec.proxy.policy_id` selects a tenant proxy
        policy; a Batch or Crawl uses one only when
        `spec.defaults.proxy.policy_id` selects a policy. A personal workspace
        may keep at most 20 schedules. Optional change detection is available
        only for single-page Scrape schedules and requires structured
        extraction. Batch and Crawl schedules reject it. The first monitored
        Scrape success establishes a baseline; later changes can emit one
        `schedule.change_detected` Webhook.
      x-required-api-key-scope: schedules:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateScheduleRequest"
      responses:
        "201":
          description: Schedule created, or a matching create replayed
          headers:
            Idempotency-Replayed:
              description: Present with value `true` for a matching replay.
              schema:
                type: string
                const: "true"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSchedule"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/schedules/{schedule_id}:
    parameters:
      - $ref: "#/components/parameters/ScheduleId"
    get:
      tags: [Schedules]
      operationId: getSchedule
      summary: Get a recurring Scrape, Batch, or Crawl schedule
      x-required-api-key-scope: schedules:read
      responses:
        "200":
          description: Schedule metadata, normalized spec, and latest run
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSchedule"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    patch:
      tags: [Schedules]
      operationId: updateSchedule
      summary: Replace selected schedule configuration
      description: |
        Updates one or more of name, cron, timezone, Scrape, Batch, or Crawl
        spec, or change detection. The optional `kind` field is an
        immutable-kind assertion and is not an update by itself. Batch and
        Crawl spec updates must carry their explicit kind. A successful change
        advances the immutable schedule/spec version used by future run
        records. Pause and resume are separate state transitions.
      x-required-api-key-scope: schedules:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateScheduleRequest"
      responses:
        "200":
          description: Schedule updated, or a matching update replayed
          headers:
            Idempotency-Replayed:
              description: Present with value `true` for a matching replay.
              schema:
                type: string
                const: "true"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSchedule"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    delete:
      tags: [Schedules]
      operationId: deleteSchedule
      summary: Permanently delete a recurring Scrape, Batch, or Crawl schedule
      description: |
        Removes the schedule definition, immutable versions, and retained run
        ledger, preventing future slots from being launched. Existing submitted
        Scrape, Batch, or Crawl jobs remain independently observable and
        cancellable through the Jobs API.
      x-required-api-key-scope: schedules:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "204":
          description: Schedule deleted, or a matching delete replayed
          headers:
            Idempotency-Replayed:
              description: Present with value `true` for a matching replay.
              schema:
                type: string
                const: "true"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/schedules/{schedule_id}/pause:
    post:
      tags: [Schedules]
      operationId: pauseSchedule
      summary: Pause future schedule launches
      description: |
        Prevents new due slots from being claimed without cancelling an
        already-submitted Scrape, Batch, or Crawl job.
      x-required-api-key-scope: schedules:write
      parameters:
        - $ref: "#/components/parameters/ScheduleId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Paused schedule, or a matching pause replayed
          headers:
            Idempotency-Replayed:
              description: Present with value `true` for a matching replay.
              schema:
                type: string
                const: "true"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSchedule"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/schedules/{schedule_id}/resume:
    post:
      tags: [Schedules]
      operationId: resumeSchedule
      summary: Resume future schedule launches
      description: |
        Computes the next future cron slot from the current server time.
        Slots missed while paused are not backfilled.
      x-required-api-key-scope: schedules:write
      parameters:
        - $ref: "#/components/parameters/ScheduleId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Active schedule, or a matching resume replayed
          headers:
            Idempotency-Replayed:
              description: Present with value `true` for a matching replay.
              schema:
                type: string
                const: "true"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSchedule"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/schedules/{schedule_id}/runs:
    get:
      tags: [Schedules]
      operationId: listScheduleRuns
      summary: List durable execution records for one schedule
      description: |
        Each cron slot has at most one run record across scheduler replicas.
        The newest 10,000 terminal records per schedule are retained alongside
        any live launch or submitted-job overlap fence. Submitted records link
        to their Scrape, Batch, or Crawl job. Once that job is terminal, its bounded
        outcome, completion time, result count, and charged credits are frozen
        on the run record so the history remains useful after job metadata
        retention. Skipped and launch-failed records expose only bounded safe
        reason codes.
      x-required-api-key-scope: schedules:read
      parameters:
        - $ref: "#/components/parameters/ScheduleId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: state
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/ScheduleRunState"
        - name: outcome
          in: query
          required: false
          description: Return only submitted runs with this frozen terminal job outcome.
          schema:
            $ref: "#/components/schemas/ScheduleJobOutcome"
      responses:
        "200":
          description: Cursor-paginated schedule runs
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScheduleRunPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/schedules/{schedule_id}/snapshots/{snapshot_id}:
    get:
      tags: [Schedules]
      operationId: getScheduleSnapshot
      summary: Get retained structured content for a monitored run
      description: |
        Returns one tenant-owned canonical structured extraction snapshot.
        Content expires with the source Job result. Hashes and changed paths
        remain on the run after content expiry, but this operation then returns
        410.
      x-required-api-key-scope: schedules:read
      parameters:
        - $ref: "#/components/parameters/ScheduleId"
        - name: snapshot_id
          in: path
          required: true
          schema:
            type: string
            pattern: "^ssnap_[0-9a-f]{32}$"
      responses:
        "200":
          description: Retained structured extraction snapshot
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataScheduleSnapshot"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/sessions:
    get:
      tags: [Sessions]
      operationId: listBrowserSessions
      summary: List persistent browser-session metadata
      description: |
        Returns cursor-paginated safe metadata only. Encrypted storage state,
        active job identity, internal workspace identity, and row versions are
        never returned.
      x-required-api-key-scope: sessions:read
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: state
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/BrowserSessionState"
        - name: engine
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/BrowserSessionEngine"
      responses:
        "200":
          description: Cursor-paginated browser sessions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BrowserSessionPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Sessions]
      operationId: createBrowserSession
      summary: Create a persistent browser session
      description: |
        Creates an empty, encrypted Playwright storage state. A referenced
        cookie jar is used only to seed the first lease. Sessions without a
        proxy policy run direct-first. Explicit proxy binding currently fails
        closed with POLICY_DENIED until sticky session-proxy execution is
        available.
      x-required-api-key-scope: sessions:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBrowserSessionRequest"
      responses:
        "201":
          description: Browser session created, or matching create replayed
          headers:
            Idempotency-Replayed:
              description: Present with value `true` for a matching replay.
              schema:
                type: string
                const: "true"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBrowserSession"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          description: Requested session policy is not executable fail-closed
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/PublicApiProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/sessions/{session_id}:
    parameters:
      - $ref: "#/components/parameters/BrowserSessionId"
    get:
      tags: [Sessions]
      operationId: getBrowserSession
      summary: Get persistent browser-session metadata
      x-required-api-key-scope: sessions:read
      responses:
        "200":
          description: Browser-session metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBrowserSession"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    delete:
      tags: [Sessions]
      operationId: closeBrowserSession
      summary: Close a persistent browser session
      description: |
        An idle session closes immediately. A session claimed by a job enters
        closing and becomes closed when that job persists its next state.
      x-required-api-key-scope: sessions:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "204":
          description: Session closed or scheduled to close
          headers:
            Idempotency-Replayed:
              description: Present with value `true` for a matching replay.
              schema:
                type: string
                const: "true"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/sessions/{session_id}/scrapes:
    post:
      tags: [Sessions, Scrapes]
      operationId: createBrowserSessionScrape
      summary: Create one version-fenced scrape in a persistent browser session
      description: |
        Requires both scrapes:write and sessions:write. The session identity is
        accepted only from the path. The expected state version prevents two
        commands from mutating the same browser context concurrently.
      x-required-api-key-scope: scrapes:write
      x-additional-required-api-key-scopes: [sessions:write]
      parameters:
        - $ref: "#/components/parameters/BrowserSessionId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Traceparent"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBrowserSessionScrapeRequest"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedJob"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/InsufficientCredits"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            Browser-session admission changed, or the idempotency key is still
            being resolved. Admission changes include
            details.conflict_kind=browser_session_admission_changed and one
            closed non-secret reason. Generic idempotency conflicts omit that
            marker and must retain the original request and key.
          content:
            application/problem+json:
              schema:
                anyOf:
                  - $ref: "#/components/schemas/BrowserSessionAdmissionConflictProblem"
                  - $ref: "#/components/schemas/PublicApiProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/sessions/{session_id}/scrapes/estimate:
    post:
      tags: [Sessions, Scrapes]
      operationId: estimateBrowserSessionScrape
      summary: Estimate a version-fenced persistent-session scrape
      description: |
        Requires both scrapes:write and sessions:write. It validates the
        session, state-version fence, engine, commercial policy, and current
        capacity without claiming the session or creating durable job state.
      x-required-api-key-scope: scrapes:write
      x-additional-required-api-key-scopes: [sessions:write]
      parameters:
        - $ref: "#/components/parameters/BrowserSessionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBrowserSessionScrapeRequest"
      responses:
        "200":
          $ref: "#/components/responses/ScrapeEstimate"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            Browser-session admission changed. The problem includes
            details.conflict_kind=browser_session_admission_changed and one
            closed non-secret reason. Refresh the session and estimate again;
            do not automatically create the scrape.
          content:
            application/problem+json:
              schema:
                anyOf:
                  - $ref: "#/components/schemas/BrowserSessionAdmissionConflictProblem"
                  - $ref: "#/components/schemas/PublicApiProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/scrapes:
    post:
      tags: [Scrapes]
      operationId: createScrape
      summary: Create an asynchronous scrape job
      description: |
        Team-bound keys may use the direct inline request subset plus active
        same-workspace exact-origin `header_secret_ids` and `cookie_jar_id`
        references. Proxy or sticky selection, persistent browser sessions,
        saved schemas, `auth_secret_id`, and Action `value_secret_id`
        references remain unavailable.
        A Team request may reference one active, verified `job.completed`
        Webhook endpoint in the same active workspace. No direct request,
        including `auto` execution, enables or falls back to a proxy unless a
        proxy policy is explicitly selected by a supported personal request.
        If Team ownership changes between the billing-owner lock and final
        workspace read, admission returns 409 with
        `details.blocker=workspace_billing_owner_changed` and
        `details.retryable=true` before claiming the Idempotency-Key or writing
        Job, outbox, reservation, or usage state. Refresh the workspace and
        estimate before manually retrying.
      x-required-api-key-scope: scrapes:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Traceparent"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateScrapeRequest"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedJob"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/InsufficientCredits"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/scrapes/estimate:
    post:
      tags: [Scrapes]
      operationId: estimateScrape
      summary: Estimate the maximum scrape credit reservation
      description: |
        Validates the same request, active commercial policy, and fresh Worker
        capability/lane capacity used by job creation, but creates no job,
        idempotency key, outbox event, credit reservation, or usage ledger line.
        The result is advisory: creation rechecks entitlement, Worker capacity,
        queue capacity, pricing, and available credits atomically.
        Team estimation also resolves any Webhook endpoint reference as an
        active, verified `job.completed` endpoint in the same active workspace.
        A concurrent Team ownership change returns the retryable
        `workspace_billing_owner_changed` 409 before entitlement, limit, or
        billing side effects; refresh the workspace before estimating again.
      x-required-api-key-scope: scrapes:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateScrapeRequest"
      responses:
        "200":
          $ref: "#/components/responses/ScrapeEstimate"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/batches:
    post:
      tags: [Scrapes]
      operationId: createBatch
      summary: Create one ordered multi-URL scrape job
      description: |
        Creates one aggregate job containing 2–100 normalized, unique GET
        requests. Requests are dispatched one at a time in input order. The
        expanded normalized request snapshots may not exceed 2 MiB, and the
        total job-attempt budget may not exceed 500. Team-bound keys may use
        the direct inline subset plus active same-workspace exact-origin
        custom-header secrets and cookie jars: no proxy/sticky,
        browser-session, saved schema, unbound authorization, or Action-value
        secret references. They may reference one
        active, verified `job.completed` Webhook endpoint in the same active
        workspace.
        If Team ownership changes between the billing-owner lock and final
        workspace read, admission returns the retryable
        `workspace_billing_owner_changed` 409 before claiming the
        Idempotency-Key or writing admission state. Refresh the workspace and
        estimate before manually retrying.
      x-required-api-key-scope: scrapes:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Traceparent"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBatchRequest"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedJob"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/InsufficientCredits"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/batches/estimate:
    post:
      tags: [Scrapes]
      operationId: estimateBatch
      summary: Estimate the maximum batch credit reservation
      description: |
        Validates the same batch contract, commercial policy, and fresh Worker
        capability/lane capacity as creation without creating a job,
        idempotency record, outbox event, reservation, usage line, or credit
        transaction. Creation rechecks all admission conditions atomically.
        Team-bound keys use the same direct inline restrictions as creation
        and any Webhook endpoint is revalidated in the same active workspace.
        A concurrent Team ownership change returns the retryable
        `workspace_billing_owner_changed` 409 before entitlement, limit, or
        billing side effects; refresh the workspace before estimating again.
      x-required-api-key-scope: scrapes:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBatchRequest"
      responses:
        "200":
          $ref: "#/components/responses/ScrapeEstimate"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/crawls:
    post:
      tags: [Scrapes]
      operationId: createCrawl
      summary: Create one bounded robots-aware same-site crawl
      description: |
        Creates a sequential GET crawl from 1–10 same-site seeds using a
        fallback http, browser, stealth, or auto execution mode plus up to 20
        ordered path-prefix engine-routing rules. The first matching rule
        selects a fixed http, browser, or stealth engine. Up to 20 ordered
        declarative LinkExtractor rules may additionally filter canonical
        absolute URLs by full-string `*`/`?` glob, domain, CSS/XPath scope,
        source tag/attribute, and explicit denied extension. Deny wins within
        one rule; rule order and document order are stable, and the first
        occurrence of a canonical URL wins across rules. Sitemap candidates
        use only URL, domain, and extension filters. Auto starts in the
        least capable pool required by the frozen render settings (ordinary
        requests start with HTTP) and may advance only after bounded blocking,
        challenge, or required-extraction evidence; it never enables a proxy.
        The lane remains direct unless defaults.proxy.policy_id is explicitly
        set. The dynamic breadth-first frontier is normalized, deduplicated,
        and bounded to 100 pages, depth 10, 15 minutes, 100 MiB, and 500 link
        candidates per page. robots.txt is mandatory, cross-site redirects are
        rejected, and requests are spaced by at least one second. Bounded robots
        Sitemap declarations and XML/urlset/index/gzip locations join the same
        frontier; each sitemap document consumes a normal crawl page, depth, and
        byte budget. A team-bound key may use this operation only with direct
        inline configuration plus active same-workspace exact-origin custom
        headers and cookie jars. Proxy/sticky selection, persistent browser
        sessions, saved extraction schemas, unbound authorization secrets, and
        Action-value secret references remain rejected. One active,
        verified `job.completed` Webhook endpoint in the same active workspace
        may be referenced.
        The selected active membership is rechecked and Team work is reserved
        against the Team owner's credit balance. Terminal Team Crawls may use
        the existing JSONL/CSV export operations without a new scope, meter, or
        database migration.
        If Team ownership changes between the billing-owner lock and final
        workspace read, admission returns the retryable
        `workspace_billing_owner_changed` 409 before claiming the
        Idempotency-Key or writing admission state. Refresh the workspace and
        estimate before manually retrying.
      x-required-api-key-scope: scrapes:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Traceparent"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCrawlRequest"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedJob"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "402":
          $ref: "#/components/responses/InsufficientCredits"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/crawls/estimate:
    post:
      tags: [Scrapes]
      operationId: estimateCrawl
      summary: Estimate the maximum crawl credit reservation
      description: |
        Validates the same crawl scope, frontier budgets, runtime references,
        active commercial policy, and fresh Worker capability/lane capacity as
        creation without writing job or billing state. Creation rechecks all
        admission conditions atomically. Team-bound keys use the same
        scrapes:write scope and only the direct inline subset documented by
        createCrawl; any Webhook endpoint is revalidated in the same active
        workspace. No new scope, usage meter, or database state is introduced
        by Team estimation.
        A concurrent Team ownership change returns the retryable
        `workspace_billing_owner_changed` 409 before entitlement, limit, or
        billing side effects; refresh the workspace before estimating again.
      x-required-api-key-scope: scrapes:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCrawlRequest"
      responses:
        "200":
          $ref: "#/components/responses/ScrapeEstimate"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/extraction-schemas:
    get:
      tags: [Extraction schemas]
      operationId: listExtractionSchemas
      summary: List tenant-owned extraction schemas
      x-required-api-key-scope: schemas:read
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: status
          in: query
          schema:
            type: string
            enum: [active, archived]
        - name: publication
          in: query
          schema:
            type: string
            enum: [published, draft, unpublished]
      responses:
        "200":
          description: Cursor-paginated extraction schemas
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExtractionSchemaPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Extraction schemas]
      operationId: createExtractionSchema
      summary: Create an extraction schema with immutable draft version 1
      x-required-api-key-scope: schemas:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Traceparent"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateExtractionSchemaRequest"
      responses:
        "200":
          description: Idempotent creation replay
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExtractionSchema"
        "201":
          description: Extraction schema created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExtractionSchema"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/extraction-schemas/{schema_id}:
    parameters:
      - $ref: "#/components/parameters/ExtractionSchemaId"
    get:
      tags: [Extraction schemas]
      operationId: getExtractionSchema
      summary: Get one extraction schema lifecycle resource
      x-required-api-key-scope: schemas:read
      responses:
        "200":
          description: Extraction schema
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExtractionSchema"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    patch:
      tags: [Extraction schemas]
      operationId: updateExtractionSchema
      summary: Rename, archive, or restore an extraction schema optimistically
      x-required-api-key-scope: schemas:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateExtractionSchemaRequest"
      responses:
        "200":
          description: Updated extraction schema
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExtractionSchema"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/extraction-schemas/{schema_id}/versions:
    parameters:
      - $ref: "#/components/parameters/ExtractionSchemaId"
    get:
      tags: [Extraction schemas]
      operationId: listExtractionSchemaVersions
      summary: List immutable schema versions newest first
      x-required-api-key-scope: schemas:read
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 10
            default: 10
        - name: before_version
          in: query
          schema:
            type: integer
            minimum: 2
            maximum: 1000001
      responses:
        "200":
          description: Bounded immutable version page
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExtractionSchemaVersionPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Extraction schemas]
      operationId: createExtractionSchemaVersion
      summary: Append an immutable draft with optimistic concurrency
      x-required-api-key-scope: schemas:write
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/Traceparent"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateExtractionSchemaVersionRequest"
      responses:
        "200":
          description: Idempotent version creation replay
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExtractionSchemaVersion"
        "201":
          description: Immutable draft appended
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExtractionSchemaVersion"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/extraction-schemas/{schema_id}/publish:
    post:
      tags: [Extraction schemas]
      operationId: publishExtractionSchema
      summary: Publish the latest immutable extraction schema version
      x-required-api-key-scope: schemas:write
      parameters:
        - $ref: "#/components/parameters/ExtractionSchemaId"
        - $ref: "#/components/parameters/Traceparent"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MoveExtractionSchemaVersionRequest"
      responses:
        "200":
          description: Publication pointer updated or already current
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExtractionSchema"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/extraction-schemas/{schema_id}/rollback:
    post:
      tags: [Extraction schemas]
      operationId: rollbackExtractionSchema
      summary: Move publication to an older immutable version
      x-required-api-key-scope: schemas:write
      parameters:
        - $ref: "#/components/parameters/ExtractionSchemaId"
        - $ref: "#/components/parameters/Traceparent"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MoveExtractionSchemaVersionRequest"
      responses:
        "200":
          description: Publication pointer rolled back
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataExtractionSchema"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs:
    get:
      tags: [Jobs]
      operationId: listJobs
      summary: List jobs for the authenticated tenant
      x-required-api-key-scope: jobs:read
      description: |
        Returns newest-first jobs with a retention-safe target summary. `search`
        performs prefix matching against either a canonical Job ID or a stored
        target URL in the authenticated workspace. Target URL searches that
        contain a query string or fragment are rejected. Target matching and
        summaries stop exposing request metadata at its immutable retention
        deadline.
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: kind
          in: query
          schema:
            $ref: "#/components/schemas/JobKind"
        - name: state
          in: query
          schema:
            $ref: "#/components/schemas/JobState"
        - name: search
          in: query
          description: |
            A `job_` prefix with 4–32 lowercase hexadecimal characters, or an
            absolute HTTP/HTTPS target URL prefix without embedded credentials.
            Target URLs containing a query string or fragment are rejected.
            The parameter must be provided at most once. Empty or
            whitespace-only input is equivalent to omitting the filter.
          schema:
            type: string
            maxLength: 256
      responses:
        "200":
          description: Cursor-paginated jobs
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/JobListItem"
                  meta:
                    $ref: "#/components/schemas/PageMeta"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}:
    get:
      tags: [Jobs]
      operationId: getJob
      summary: Get job state, progress, usage, and terminal error
      x-required-api-key-scope: jobs:read
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "200":
          description: Job
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataJob"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/events:
    get:
      tags: [Jobs]
      operationId: streamJobEvents
      summary: Stream resumable job and attempt lifecycle events
      x-required-api-key-scope: jobs:read
      description: |
        Returns a bounded Server-Sent Events connection. The stream emits a
        safe `job` snapshot when state or progress changes, resumable `attempt`
        lifecycle events, periodic comment heartbeats, and an `end` event for a
        terminal or unavailable job. It closes after at most 25 seconds with a
        `reconnect` event so clients reconnect under the normal API-key rate
        limit. Send the last received attempt event ID in `Last-Event-ID` to
        continue after that durable event. Internal event payloads are never
        exposed. Crawl snapshots include the same bounded discovered-link,
        truncated-page, reached-depth, and pending-frontier aggregate as the
        Job resource. Event access expires with job metadata retention.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - name: Last-Event-ID
          in: header
          required: false
          description: Last durable attempt event ID received from this job.
          schema:
            type: string
            pattern: "^aevt_[0-9a-f]{32}$"
      responses:
        "200":
          description: Bounded Server-Sent Events connection
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            text/event-stream:
              schema:
                type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/cancel:
    post:
      tags: [Jobs]
      operationId: cancelJob
      summary: Request cancellation
      x-required-api-key-scope: jobs:cancel
      parameters:
        - $ref: "#/components/parameters/JobId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedJob"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/pause:
    post:
      tags: [Jobs]
      operationId: pauseJob
      summary: Pause queued work or drain an in-flight attempt
      x-required-api-key-scope: jobs:cancel
      description: |
        Pauses scrape, batch, and crawl jobs. A queued job becomes paused
        immediately. A leased or running job becomes pausing; its current
        attempt keeps its existing lease and may heartbeat and finish, after
        which no next request is dispatched and the job becomes paused.
        Repeating the same Idempotency-Key replays the accepted response.
        Cancellation has priority over pause and resume.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedJob"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/resume:
    post:
      tags: [Jobs]
      operationId: resumeJob
      summary: Resume a paused job
      x-required-api-key-scope: scrapes:write
      description: |
        Moves a paused scrape, batch, or crawl job back to queued and stages
        exactly one dispatchable accepted event. Persisted retry backoff and
        crawl request delay remain effective. Time spent pausing or paused is
        excluded from the crawl active-duration budget. Pausing, cancelling,
        terminal, and unsupported job kinds cannot be resumed. Repeating the
        same Idempotency-Key replays the accepted response.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedJob"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/replay:
    post:
      tags: [Jobs]
      operationId: replayJob
      summary: Create a new billable job from one terminal job specification
      x-required-api-key-scope: scrapes:write
      description: |
        Reconstructs the source scrape, batch, or crawl from its immutable
        normalized specification and submits it as a new job. Replay never
        requeues the old row and never reuses its reservation, pricing,
        entitlement, proxy, credential, cookie-jar, or webhook decision.
        Current admission, credit, limit, ownership, active-resource, and
        outbound-policy checks run again. Only terminal jobs whose request
        metadata is still retained and whose specification version remains
        supported can be replayed. Specification v2 and v3 snapshots are
        supported: v2 retains its historical raw-header shape, while v3 can
        retain origin-bound header-secret references. Every referenced secret
        is checked against its current active state and binding; its value is
        never returned. The operation is audited and requires an
        Idempotency-Key because it can contact targets and consume credits. If
        the retained request uses POST or contains click, press, or
        double_click, this call explicitly authorizes another
        side-effect-capable target execution; reconcile any prior
        SIDE_EFFECT_UNCONFIRMED outcome before replaying it. Team-bound keys may
        replay terminal direct-inline scrape, Batch, and Crawl jobs only in
        their bound active workspace; retained same-workspace exact-origin
        custom-header and cookie-jar references are revalidated, while proxy,
        session, saved-schema, unbound authorization, and Action-value
        references remain rejected. A retained Webhook
        endpoint reference is revalidated as active, verified,
        `job.completed`, and bound to that same active workspace.
        A Team owner change during admission returns the retryable
        `workspace_billing_owner_changed` 409 before the replay Idempotency-Key
        or any new Job/billing state is written; refresh workspace state before
        manually retrying.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "202":
          $ref: "#/components/responses/AcceptedJob"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/usage:
    get:
      tags: [Jobs]
      operationId: getJobUsage
      summary: Get itemized usage and credit settlement
      deprecated: true
      x-required-api-key-scope: usage:read
      description: |
        Deprecated unpaginated compatibility view. New clients use
        `/v1/jobs/{job_id}/usage/summary` for bounded billing state and
        `/v1/jobs/{job_id}/usage/items` for cursor-paginated ledger lines.

        Itemized usage shares the job metadata access window. At or after that
        immutable deadline, access fails with 410 even if the asynchronous
        ledger-retention pass has not yet deleted the underlying lines. To
        prevent an unbounded response, this legacy operation fails with 413
        `LIMIT_EXCEEDED` when the ledger is too large for the compatibility
        view. Closed paid-period statements remain available from their frozen
        snapshots.
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "200":
          description: Tenant-scoped job usage
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataJobUsage"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "413":
          $ref: "#/components/responses/UsageLedgerTooLarge"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/usage/summary:
    get:
      tags: [Jobs]
      operationId: getJobUsageSummary
      summary: Get bounded job billing and settlement state
      x-required-api-key-scope: usage:read
      description: |
        Returns fixed-size reservation and settlement totals without loading
        itemized ledger lines. This is the polling-safe usage resource for live
        Jobs and Console views. The resource shares the Job metadata access
        window and fails with 410 after its immutable retention deadline.
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "200":
          description: Tenant-scoped bounded job usage summary
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataJobUsageSummary"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/usage/items:
    get:
      tags: [Jobs]
      operationId: listJobUsageItems
      summary: List cursor-paginated job usage ledger lines
      x-required-api-key-scope: usage:read
      description: |
        Returns at most 100 immutable ledger lines in their durable job order.
        `next_cursor` is opaque and bound to the authorized Job; pass it
        unchanged to continue. Tenant authorization precedes cursor resolution.
        Itemized usage shares the Job metadata access window and fails with 410
        after its immutable retention deadline. Pagination becomes available
        only after the Job reaches a terminal state and its usage reservation
        is settled or released, meaning terminal usage settlement has
        completed. Until both conditions hold, a read fails with 409 and
        clients should poll the bounded summary instead.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Cursor-paginated tenant-scoped job usage ledger lines
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobUsageItemPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: |
            The Job is not terminal, or its terminal usage settlement has not
            completed with the usage reservation settled or released.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/PublicApiProblem"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/billing/balance:
    get:
      tags: [Billing]
      operationId: getBillingBalance
      summary: Get the current available and reserved credit balance
      description: |
        Returns one tenant-scoped statement snapshot containing credits that
        are currently available for new reservations and the aggregate/count
        of reservations that remain active. `nearest_expiration` considers
        only currently available grant credits with a finite future expiry.
        It is null when no such grant exists, including when the balance is
        zero or every available grant is non-expiring. Internal user,
        workspace, job, reservation, and credit-ledger identifiers are never
        returned.
      x-required-api-key-scope: usage:read
      responses:
        "200":
          description: Current tenant credit balance
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataBillingBalance"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/billing/periods:
    get:
      tags: [Billing]
      operationId: listBillingPeriodStatements
      summary: List ledger-backed paid billing-period statements
      description: |
        Returns paid subscription periods newest first. Usage is grouped by
        immutable pricing version, meter, unit, and unit price using the
        half-open `[period_start, period_end)` occurrence window. Reservation
        totals describe jobs created in that period and can therefore differ
        from occurrence-based metered credits when work crosses a boundary.
        Attribution follows the immutable billing user on each reservation, so
        a personal key receives payer-wide personal and owner-funded Team
        totals. Closed periods are frozen no earlier than one 24-hour
        reservation TTL after period_end using a read-consistent transaction.
        Historical cross-subscription overlap for one payer fails closed.
        These records reconcile usage and credits; provider-issued tax invoices
        remain authoritative for tax and payment documents. Payment amounts
        carry the immutable 0..3 currency exponent frozen on the paid period;
        clients must not infer it from locale or currency metadata.
      x-required-api-key-scope: usage:read
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Cursor-paginated paid billing-period statements
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingPeriodStatementPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/billing/adjustments:
    get:
      tags: [Billing]
      operationId: listBillingAdjustments
      summary: List provider-verified refunds and disputes
      description: |
        Returns immutable normalized refund and dispute events newest first.
        Credit effects are cumulative, exact, idempotent, and restricted to the
        matched paid order's own grant, including credit-pack grants. Any
        unrecovered credits create a refund_outstanding hold. A fully recovered
        credit-pack refund needs no refund hold, while a full contract refund
        creates refund_full to block its otherwise-live entitlement. Open or
        lost disputes may hold access, and won disputes release only their
        matching dispute hold. An active hold denies new scrape admission and
        dispatch.
        Each amount includes the 0..3 currency exponent frozen on its
        authoritative paid order; clients must not infer it from locale or
        currency metadata.
      x-required-api-key-scope: usage:read
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Cursor-paginated billing adjustment events
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BillingAdjustmentPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/results:
    get:
      tags: [Jobs]
      operationId: listJobResults
      summary: List normalized job results
      x-required-api-key-scope: results:read
      description: |
        Returns at most 100 normalized results in their durable per-job
        publication order. While an authorized job is nonterminal, every
        nonempty page returns an opaque next_cursor for its last row, including
        a page that has reached the currently visible tail. A subsequent empty
        tail poll returns an empty data array and a null next_cursor; incremental
        pollers retain and reuse their last nonempty cursor on the next timer
        tick. When the job becomes terminal, that same cursor remains valid and
        ordinary stable pagination ends with a null next_cursor.

        Result publication is serialized by PostgreSQL independently of
        application-replica clocks, so feeding each returned nonempty cursor
        into the next read neither skips nor repeats committed rows. Cursors
        are signed and bound to this job, and tenant authorization is checked
        before the cursor anchor is resolved. Cross-job, cross-tenant, missing,
        and modified cursors are rejected. Once the plan version's frozen
        result-retention deadline is reached, access fails with 410 even if the
        asynchronous minimization pass has not yet cleared stored bodies.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Incremental or stable cursor-paginated results
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Result"
                  meta:
                    $ref: "#/components/schemas/PageMeta"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/results/events:
    get:
      tags: [Jobs]
      operationId: streamJobResults
      summary: Stream committed Results in durable order with anchored resume
      x-required-api-key-scope: results:read
      description: |
        Returns a bounded Server-Sent Events connection over committed Results
        in the same durable per-job publication order as `GET
        /v1/jobs/{job_id}/results`. Each `result` event has `id: res_...` and
        its data is the complete public `Result` resource, including visible
        artifact metadata. Clients persist that event ID only after processing
        the Result and send it as `Last-Event-ID` on the next connection. The
        server then resumes strictly after that retained Result; it never emits
        the anchor again. The anchor must identify a Result belonging to this
        authorized Job. A malformed, unknown, or wrong-Job anchor returns 400,
        while cross-tenant Jobs remain hidden with 404.

        Delivery before an acknowledged `Last-Event-ID` is at least once: if a
        client processes a Result but disconnects before durably saving its ID,
        that Result can be delivered again. Consumers must make processing
        idempotent and deduplicate by `res_...`. The no-anchor-replay guarantee
        applies only after the client supplies a valid acknowledged anchor.

        Every connection first sets the SSE retry interval to 2000 milliseconds.
        Idle connections receive comment heartbeats. A nonterminal connection
        lasts at most 25 seconds, then emits an ID-less `reconnect` event with
        `{reason:"connection_window_elapsed",retry_after_ms:2000,last_event_id}`
        and closes. Once the Job is terminal and all committed Results after the
        supplied anchor have been delivered, the stream emits an ID-less `end`
        event with `{reason:"terminal",last_event_id}` and closes. If authorized
        result content becomes unavailable during an open connection, `end`
        uses reason `unavailable`. A polling failure after headers emits an
        ID-less `error` event with
        `{code:"STREAM_UNAVAILABLE",retryable:true}` before closing; reconnect
        from the last processed Result.

        Opening and reopening the stream consumes normal API-key rate limits
        and bounded concurrent-stream capacity. Access is governed by Result
        content retention, not metadata retention. A connection attempt after
        the frozen Result-retention deadline returns 410. The response disables
        storage, intermediary transformation, and proxy buffering.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - name: Last-Event-ID
          in: header
          required: false
          description: |
            Last completely processed `result` event ID from this Job. The next
            connection resumes strictly after this retained Result.
          schema:
            type: string
            pattern: "^res_[0-9a-f]{32}$"
      responses:
        "200":
          description: Bounded resumable Server-Sent Events Result connection
          headers:
            Cache-Control:
              required: true
              schema:
                type: string
                const: no-store, no-transform
            X-Accel-Buffering:
              required: true
              schema:
                type: string
                const: "no"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  SSE frames containing `result`, `reconnect`, `end`, or
                  retryable `error` events plus comment heartbeats.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/jobs/{job_id}/exports:
    get:
      tags: [Exports]
      operationId: listJobExports
      summary: List recent exports for a terminal batch or crawl
      x-required-api-key-scope: results:read
      description: |
        Returns the newest authorized exports first. The list is bounded and
        has no cursor. It includes active, terminal, and expired export
        resources so clients can recover asynchronous state after a reload.
        Personal and Team-bound keys may address terminal Batch or Crawl jobs.
        Team single-scrape exports remain rejected. A Team issuer must remain
        an active member of the bound active workspace.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Bounded newest-first export list
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobExportList"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Exports]
      operationId: createJobExport
      summary: Queue an immutable JSONL or CSV result export
      x-required-api-key-scope: scrapes:write
      description: |
        Available for terminal Batch and Crawl jobs in personal or active Team
        workspaces while retained result content is still available. Team
        single-scrape exports remain rejected. The accepted export freezes the
        final request-result selection and row count. Generation is
        asynchronous; poll the returned resource or its status route, then
        explicitly follow download_url only after state becomes ready. Team
        support adds no API-key scope, usage meter, or database migration.
      parameters:
        - $ref: "#/components/parameters/JobId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateJobExportRequest"
      responses:
        "202":
          description: Export queued, or matching idempotent request replayed
          headers:
            Idempotency-Replayed:
              description: Present with value `true` for a matching replay.
              schema:
                type: string
                const: "true"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataJobExport"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/exports/{export_id}:
    get:
      tags: [Exports]
      operationId: getJobExport
      summary: Get asynchronous export status
      x-required-api-key-scope: results:read
      description: |
        Returns status only from the API key's bound active workspace. An
        active Team member may inspect a terminal Team Batch or Crawl export
        shared by that workspace; suspended, closed, or foreign access remains
        hidden.
      parameters:
        - $ref: "#/components/parameters/ExportId"
      responses:
        "200":
          description: Authorized export status
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataJobExport"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/exports/{export_id}/download:
    get:
      tags: [Exports]
      operationId: downloadJobExport
      summary: Download a ready export through the authenticated application
      x-required-api-key-scope: artifacts:read
      description: |
        Returns bytes only while the authorized export is ready and unexpired.
        Queued, generating, retry-scheduled, and failed exports return 409;
        expired exports return 410. The endpoint never redirects to or exposes
        a private object-store locator. Team-bound keys may download terminal
        Batch or Crawl exports only while the issuer and workspace remain
        active. Workspace closure fences retained exports, and source-result or
        export retention expiration still blocks download. Every JSONL object
        and CSV data row includes nullable crawl_depth, populated for Crawl
        requests and null for Batch requests.
      parameters:
        - $ref: "#/components/parameters/ExportId"
      responses:
        "200":
          description: Authenticated JSONL or CSV attachment
          headers:
            Cache-Control:
              required: true
              schema:
                type: string
                const: private, no-store
            Content-Disposition:
              required: true
              schema:
                type: string
                pattern: '^attachment; filename="toptrends-job_[0-9a-f]{32}\.(jsonl|csv)"$'
            Content-Length:
              required: true
              schema:
                type: integer
                minimum: 0
                maximum: 33554432
            Content-Security-Policy:
              required: true
              schema:
                type: string
                const: "sandbox; default-src 'none'"
            Cross-Origin-Resource-Policy:
              required: true
              schema:
                type: string
                const: same-origin
            X-Content-Type-Options:
              required: true
              schema:
                type: string
                const: nosniff
            X-Export-SHA256:
              required: true
              schema:
                type: string
                pattern: "^[a-f0-9]{64}$"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/x-ndjson:
              schema:
                type: string
                format: binary
            text/csv:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/secrets:
    get:
      tags: [Secrets]
      operationId: listSecrets
      summary: List target-secret metadata without values
      description: |
        The API key binding selects exactly one personal or Team workspace.
        Active Team members may read metadata in that workspace; resources from
        any other workspace are never aggregated into the page.
      x-required-api-key-scope: secrets:read
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/TargetCredentialStatus"
      responses:
        "200":
          description: Cursor-paginated secret metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SecretPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Secrets]
      operationId: createSecret
      summary: Store a write-only target secret
      description: |
        Personal workspaces may create every supported kind. Team workspaces
        accept only an exact-origin `header_value` secret and require the
        issuer's current owner or admin role. Unbound `authorization` and
        `action_value` values remain personal-only.
      x-required-api-key-scope: secrets:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSecretRequest"
      responses:
        "201":
          description: Secret metadata; the submitted value is never returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSecret"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/secrets/{secret_id}:
    get:
      tags: [Secrets]
      operationId: getSecret
      summary: Get target-secret metadata without its value
      description: |
        Resolves only inside the API key's bound workspace. Any active Team
        member may read safe metadata.
      x-required-api-key-scope: secrets:read
      parameters:
        - $ref: "#/components/parameters/SecretId"
      responses:
        "200":
          description: Secret metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSecret"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    patch:
      tags: [Secrets]
      operationId: updateSecret
      summary: Rename, rotate, enable, or disable a target secret
      description: |
        Resolves only inside the bound workspace. A Team mutation requires the
        issuer's current owner or admin role and records metadata-only audit
        evidence; the submitted value is never included.
      x-required-api-key-scope: secrets:write
      parameters:
        - $ref: "#/components/parameters/SecretId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateSecretRequest"
      responses:
        "200":
          description: Updated metadata; rotated values are never returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataSecret"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    delete:
      tags: [Secrets]
      operationId: deleteSecret
      summary: Revoke and delete a target secret
      description: |
        Resolves only inside the bound workspace. A Team mutation requires the
        issuer's current owner or admin role.
      x-required-api-key-scope: secrets:write
      parameters:
        - $ref: "#/components/parameters/SecretId"
      responses:
        "204":
          description: Secret revoked
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/cookie-jars:
    get:
      tags: [Secrets]
      operationId: listCookieJars
      summary: List origin-bound cookie-jar metadata without cookie values
      description: |
        The API key binding selects exactly one personal or Team workspace.
        Active Team members may read metadata in that workspace.
      x-required-api-key-scope: secrets:read
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/TargetCredentialStatus"
      responses:
        "200":
          description: Cursor-paginated cookie-jar metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CookieJarPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Secrets]
      operationId: createCookieJar
      summary: Store cookies bound to one public HTTP(S) origin
      description: |
        Team creation requires the issuer's current owner or admin role. The
        cookie values remain write-only and the jar's identity, workspace, and
        exact origin cannot be rebound after creation.
      x-required-api-key-scope: secrets:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCookieJarRequest"
      responses:
        "201":
          description: Cookie-jar metadata; cookie values are never returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataCookieJar"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/cookie-jars/{cookie_jar_id}:
    get:
      tags: [Secrets]
      operationId: getCookieJar
      summary: Get cookie-jar metadata without cookie values
      description: |
        Resolves only inside the API key's bound workspace. Any active Team
        member may read safe metadata.
      x-required-api-key-scope: secrets:read
      parameters:
        - $ref: "#/components/parameters/CookieJarId"
      responses:
        "200":
          description: Cookie-jar metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataCookieJar"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    patch:
      tags: [Secrets]
      operationId: updateCookieJar
      summary: Replace cookies, rename, enable, or disable a cookie jar
      description: |
        Resolves only inside the bound workspace. A Team mutation requires the
        issuer's current owner or admin role and records metadata-only audit
        evidence.
      x-required-api-key-scope: secrets:write
      parameters:
        - $ref: "#/components/parameters/CookieJarId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateCookieJarRequest"
      responses:
        "200":
          description: Updated metadata; cookie values are never returned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataCookieJar"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    delete:
      tags: [Secrets]
      operationId: deleteCookieJar
      summary: Revoke and delete a cookie jar
      description: |
        Resolves only inside the bound workspace. A Team mutation requires the
        issuer's current owner or admin role.
      x-required-api-key-scope: secrets:write
      parameters:
        - $ref: "#/components/parameters/CookieJarId"
      responses:
        "204":
          description: Cookie jar revoked
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/proxy-policies:
    get:
      tags: [Proxy policies]
      operationId: listProxyPolicies
      summary: List proxy policies for the authenticated tenant
      x-required-api-key-scope: proxies:read
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: status
          in: query
          required: false
          schema:
            $ref: "#/components/schemas/ProxyPolicyStatus"
      responses:
        "200":
          description: Cursor-paginated proxy policies
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProxyPolicyPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Proxy policies]
      operationId: createProxyPolicy
      summary: Create a managed Webshare or BYO proxy policy
      x-required-api-key-scope: proxies:write
      description: |
        Managed Webshare policies select from platform-managed inventory. BYO
        credentials are accepted only in the request, encrypted at rest, and
        never returned by this API.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateProxyPolicyRequest"
      responses:
        "201":
          description: Proxy policy created
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataProxyPolicy"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/proxy-policies/{policy_id}:
    get:
      tags: [Proxy policies]
      operationId: getProxyPolicy
      summary: Get a proxy policy
      x-required-api-key-scope: proxies:read
      parameters:
        - $ref: "#/components/parameters/ProxyPolicyId"
      responses:
        "200":
          description: Proxy policy
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataProxyPolicy"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    patch:
      tags: [Proxy policies]
      operationId: updateProxyPolicy
      summary: Update a proxy policy
      x-required-api-key-scope: proxies:write
      description: |
        Provider and pool kind are immutable. The endpoint field is accepted
        only for BYO policies, and endpoint credentials are never returned.
      parameters:
        - $ref: "#/components/parameters/ProxyPolicyId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateProxyPolicyRequest"
      responses:
        "200":
          description: Proxy policy updated
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataProxyPolicy"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    delete:
      tags: [Proxy policies]
      operationId: deleteProxyPolicy
      summary: Delete a proxy policy
      x-required-api-key-scope: proxies:write
      description: Active allocations must be released before deletion.
      parameters:
        - $ref: "#/components/parameters/ProxyPolicyId"
      responses:
        "204":
          description: Proxy policy deleted
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/webhook-endpoints:
    get:
      tags: [Webhooks]
      operationId: listWebhookEndpoints
      summary: List webhook endpoints for the authenticated tenant
      description: |
        Results are isolated to the selected personal or Team workspace.
        Every active Team member with `webhooks:read` may read endpoint
        metadata; signing secrets are never returned.
      x-required-api-key-scope: webhooks:read
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/WebhookEndpointStatus"
      responses:
        "200":
          description: Cursor-paginated webhook endpoints
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpointPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    post:
      tags: [Webhooks]
      operationId: createWebhookEndpoint
      summary: Create a webhook endpoint and reveal its signing secret once
      description: |
        Creates the endpoint in the selected personal or Team workspace.
        Current Team owner or admin membership and `webhooks:write` are
        required; the signing secret is revealed only in this response.
      x-required-api-key-scope: webhooks:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebhookEndpointRequest"
      responses:
        "201":
          description: Pending endpoint and one-time signing secret
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataCreatedWebhookEndpoint"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "415":
          $ref: "#/components/responses/UnsupportedMediaType"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/webhook-endpoints/{endpoint_id}:
    get:
      tags: [Webhooks]
      operationId: getWebhookEndpoint
      summary: Get a webhook endpoint without its signing secret
      description: |
        Resolves only inside the selected personal or Team workspace. Every
        active Team member with `webhooks:read` may read endpoint metadata.
      x-required-api-key-scope: webhooks:read
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      responses:
        "200":
          description: Webhook endpoint
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataWebhookEndpoint"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    patch:
      tags: [Webhooks]
      operationId: updateWebhookEndpoint
      summary: Rename, disable, or replace the URL of a webhook endpoint
      description: |
        Replacing the URL resets the endpoint to pending verification. Team
        mutations require current owner or admin membership in the selected
        active workspace and `webhooks:write`.
      x-required-api-key-scope: webhooks:write
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWebhookEndpointRequest"
      responses:
        "200":
          description: Updated webhook endpoint
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataWebhookEndpoint"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
    delete:
      tags: [Webhooks]
      operationId: deleteWebhookEndpoint
      summary: Delete a webhook endpoint and cancel pending deliveries
      description: |
        Team deletion requires current owner or admin membership in the
        selected active workspace and `webhooks:write`.
      x-required-api-key-scope: webhooks:write
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      responses:
        "204":
          description: Webhook endpoint deleted
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/webhook-endpoints/{endpoint_id}/verify:
    post:
      tags: [Webhooks]
      operationId: verifyWebhookEndpoint
      summary: Prove endpoint control with an outbound signed challenge
      description: |
        Sends one signed `webhook.endpoint_verification` event. The endpoint
        must return JSON containing only the same `challenge` value. Redirects
        and any private, special, or mixed public/private DNS answer are rejected.
        Team verification requires current owner or admin membership both
        before and after the external challenge, in the same selected active
        workspace.
      x-required-api-key-scope: webhooks:write
      parameters:
        - $ref: "#/components/parameters/WebhookEndpointId"
      responses:
        "200":
          description: Verified active webhook endpoint
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataWebhookEndpoint"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          description: Endpoint verification failed
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/PublicApiProblem"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/webhook-deliveries:
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: List safe webhook delivery diagnostics for the authenticated tenant
      description: |
        Results are isolated to the selected personal or Team workspace. Every
        active Team member with `webhooks:read` may read them. Payloads,
        signatures, receiver URLs, response bodies, and claim tokens are never
        returned.
      x-required-api-key-scope: webhooks:read
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: state
          in: query
          schema:
            $ref: "#/components/schemas/WebhookDeliveryState"
        - name: endpoint_id
          in: query
          schema:
            type: string
            pattern: "^wh_[0-9a-f]{32}$"
        - name: job_id
          in: query
          schema:
            type: string
            pattern: "^job_[0-9a-f]{32}$"
      responses:
        "200":
          description: Cursor-paginated webhook delivery metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDeliveryPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/webhook-deliveries/{delivery_id}:
    get:
      tags: [Webhooks]
      operationId: getWebhookDelivery
      summary: Get one safe webhook delivery diagnostic resource
      description: |
        Resolves only inside the selected personal or Team workspace. Every
        active Team member with `webhooks:read` may read this safe metadata.
      x-required-api-key-scope: webhooks:read
      parameters:
        - $ref: "#/components/parameters/WebhookDeliveryId"
      responses:
        "200":
          description: Webhook delivery metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataWebhookDelivery"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/webhook-deliveries/{delivery_id}/replay:
    post:
      tags: [Webhooks]
      operationId: replayWebhookDelivery
      summary: Requeue one dead-letter webhook delivery
      description: |
        Requeues the same stable event ID and payload after resetting its
        per-cycle attempt budget. The endpoint must still be verified and
        active, and the immutable body must remain inside its retention window.
        The operation is workspace-scoped, idempotent, and appended to the
        security audit log. Team replay requires current owner or admin
        membership in the selected active workspace and `webhooks:write`;
        succeeded, cancelled, in-flight, already queued, and payload-expired
        deliveries cannot be replayed with a new idempotency key.
      x-required-api-key-scope: webhooks:write
      parameters:
        - $ref: "#/components/parameters/WebhookDeliveryId"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "202":
          description: Dead-letter delivery requeued, or matching idempotent request replayed
          headers:
            Idempotency-Replayed:
              description: Present with value `true` when this response replays an earlier matching request.
              schema:
                type: string
                const: "true"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataWebhookDelivery"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/artifacts/{artifact_id}:
    get:
      tags: [Artifacts]
      operationId: getArtifactMetadata
      summary: Get authorized artifact metadata
      x-required-api-key-scope: artifacts:read
      description: Storage provider, bucket, and object keys are never exposed.
      parameters:
        - $ref: "#/components/parameters/ArtifactId"
      responses:
        "200":
          description: Artifact metadata
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DataArtifactMetadata"
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /v1/artifacts/{artifact_id}/download:
    get:
      tags: [Artifacts]
      operationId: downloadArtifact
      summary: Download an artifact through the authenticated application
      x-required-api-key-scope: artifacts:read
      description: |
        The application authorizes every request and streams bytes from its
        private local, S3-compatible, or R2 backend. This endpoint never
        redirects to or returns a presigned object-store URL.
      parameters:
        - $ref: "#/components/parameters/ArtifactId"
      responses:
        "200":
          description: Authenticated artifact byte stream
          headers:
            Cache-Control:
              required: true
              schema:
                type: string
                const: private, no-store
            Content-Disposition:
              required: true
              schema:
                type: string
            Content-Length:
              required: true
              schema:
                type: integer
                minimum: 0
            Content-Type:
              required: true
              schema:
                type: string
            X-Artifact-SHA256:
              required: true
              schema:
                type: string
                pattern: "^[a-f0-9]{64}$"
            X-Content-Type-Options:
              required: true
              schema:
                type: string
                const: nosniff
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-RateLimit-Limit:
              $ref: "#/components/headers/XRateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/XRateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/XRateLimitReset"
          content:
            "*/*":
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthenticated"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          $ref: "#/components/responses/Gone"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

webhooks:
  jobCompleted:
    post:
      tags: [Webhooks]
      operationId: receiveJobCompletedWebhook
      summary: Deliver one terminal job summary
      description: |
        The event ID and raw JSON body remain stable across at-least-once
        retries. Verify the HMAC over `id.timestamp.raw_body`, reject stale
        timestamps, and deduplicate the event ID before applying side effects.
      security: []
      parameters:
        - $ref: "#/components/parameters/WebhookIdHeader"
        - $ref: "#/components/parameters/WebhookAttemptHeader"
        - $ref: "#/components/parameters/WebhookTimestampHeader"
        - $ref: "#/components/parameters/WebhookSignatureHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JobCompletedWebhookEvent"
      responses:
        "2XX":
          description: Event durably accepted
  scheduleChangeDetected:
    post:
      tags: [Webhooks]
      operationId: receiveScheduleChangeDetectedWebhook
      summary: Deliver one structured schedule change
      description: |
        The event ID, comparison hashes, changed JSON Pointer paths, and raw
        JSON body remain stable across at-least-once retries. Retrieve snapshot
        content through the authenticated API before its retention deadline.
      security: []
      parameters:
        - $ref: "#/components/parameters/WebhookIdHeader"
        - $ref: "#/components/parameters/WebhookAttemptHeader"
        - $ref: "#/components/parameters/WebhookTimestampHeader"
        - $ref: "#/components/parameters/WebhookSignatureHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScheduleChangeDetectedWebhookEvent"
      responses:
        "2XX":
          description: Event durably accepted
  endpointVerification:
    post:
      tags: [Webhooks]
      operationId: receiveWebhookEndpointVerification
      summary: Verify control of a webhook endpoint
      description: Return JSON containing only the received challenge value.
      security: []
      parameters:
        - $ref: "#/components/parameters/WebhookIdHeader"
        - $ref: "#/components/parameters/WebhookAttemptHeader"
        - $ref: "#/components/parameters/WebhookTimestampHeader"
        - $ref: "#/components/parameters/WebhookSignatureHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEndpointVerificationEvent"
      responses:
        "2XX":
          description: Exact verification challenge response
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [challenge]
                properties:
                  challenge:
                    type: string
                    pattern: "^whv_[A-Za-z0-9_-]{43}$"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: TopTrendsAI API key
      description: |
        A personal-workspace `tt_live_...` key or team-workspace
        `tt_team_...` key. Each operation declares its required capability in
        `x-required-api-key-scope`; keys without it receive 403. Team keys are
        bound to one team workspace and support the direct scrape, bounded
        Batch, bounded robots-aware Crawl, terminal Team Batch/Crawl export,
        workspace-isolated Webhook endpoint/delivery, and origin-bound
        custom-header/cookie credential operation subset documented by this
        contract. Single-scrape exports, proxies, sessions, saved schemas, and
        unbound authorization/Action secrets remain unsupported for Team
        workspaces.
    consoleSession:
      type: apiKey
      in: cookie
      name: better-auth.session_token
      description: |
        Better Auth same-origin console session cookie. Secure deployments may
        apply the standard `__Secure-` cookie prefix. Mutations also require a
        trusted Origin and reject cross-site Fetch Metadata.

  headers:
    PrivateNoStore:
      description: Authenticated user-specific content must not be cached
      required: true
      schema:
        type: string
        const: private, no-store
    XTraceId:
      description: Bounded diagnostic trace ID; never an authorization identity
      required: true
      schema:
        type: string
        pattern: "^[0-9a-f]{32}$"
    RateLimitLimit:
      description: Maximum requests allowed for this API key scope in the current window
      required: true
      schema:
        type: integer
        minimum: 1
    RateLimitRemaining:
      description: Requests remaining for this API key scope in the current window
      required: true
      schema:
        type: integer
        minimum: 0
    RateLimitReset:
      description: Whole seconds until the current rate-limit window resets
      required: true
      schema:
        type: integer
        minimum: 1
    RateLimitPolicy:
      description: Applied fixed-window policy in `limit;w=seconds` form
      required: true
      schema:
        type: string
        pattern: "^[1-9][0-9]*;w=[1-9][0-9]*$"
    XRateLimitLimit:
      description: Compatibility alias of `RateLimit-Limit`
      required: true
      schema:
        type: integer
        minimum: 1
    XRateLimitRemaining:
      description: Compatibility alias of `RateLimit-Remaining`
      required: true
      schema:
        type: integer
        minimum: 0
    XRateLimitReset:
      description: Unix timestamp in whole seconds when the current window resets
      required: true
      schema:
        type: integer
        minimum: 1
    RetryAfter:
      description: Whole seconds to wait before retrying the rate-limited operation
      required: true
      schema:
        type: integer
        minimum: 1

  parameters:
    WebhookIdHeader:
      name: webhook-id
      in: header
      required: true
      schema:
        type: string
        pattern: "^evt_[0-9a-f]{32}$"
    WebhookAttemptHeader:
      name: webhook-attempt
      in: header
      required: true
      schema:
        type: integer
        minimum: 1
        maximum: 20
    WebhookTimestampHeader:
      name: webhook-timestamp
      in: header
      required: true
      schema:
        type: integer
        minimum: 0
    WebhookSignatureHeader:
      name: webhook-signature
      in: header
      required: true
      schema:
        type: string
        pattern: "^v1=[A-Za-z0-9_-]{43}$"
    Traceparent:
      name: traceparent
      in: header
      required: false
      description: |
        W3C version-00 context accepted for asynchronous diagnostic correlation.
        Invalid, unsupported, or all-zero values are replaced with a new context.
      schema:
        type: string
        pattern: "^00-[0-9a-fA-F]{32}-[0-9a-fA-F]{16}-0[01]$"
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Unique for the authenticated tenant and operation.
      schema:
        type: string
        minLength: 16
        maxLength: 128
        pattern: "^[A-Za-z0-9._:-]+$"
    Cursor:
      name: cursor
      in: query
      required: false
      schema:
        type: string
        maxLength: 512
    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    JobId:
      name: job_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^job_[A-Za-z0-9]+$"
    ExtractionSchemaId:
      name: schema_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^xs_[0-9a-f]{32}$"
    SecretId:
      name: secret_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^sec_[0-9a-f]{32}$"
    CookieJarId:
      name: cookie_jar_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^cj_[0-9a-f]{32}$"
    BrowserSessionId:
      name: session_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^ses_[0-9a-f]{32}$"
    ScheduleId:
      name: schedule_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^sch_[0-9a-f]{32}$"
    ProxyPolicyId:
      name: policy_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^pp_[0-9a-f]{32}$"
    WebhookEndpointId:
      name: endpoint_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^wh_[0-9a-f]{32}$"
    WebhookDeliveryId:
      name: delivery_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^whd_[0-9a-f]{32}$"
    ArtifactId:
      name: artifact_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^art_[0-9a-f]{32}$"
    ExportId:
      name: export_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^exp_[0-9a-f]{32}$"
    WorkspaceId:
      name: workspace_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^ws_[0-9a-f]{32}$"
    WorkspaceInvitationId:
      name: invitation_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^wsi_[0-9a-f]{32}$"
    WorkspaceMemberId:
      name: member_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^wm_[0-9a-f]{32}$"
    WorkspaceListCursor:
      name: cursor
      in: query
      required: false
      description: Opaque cursor bound to the selected status filter.
      schema:
        type: string
        minLength: 1
        maxLength: 512
        pattern: "^[A-Za-z0-9_-]+$"
    WorkspaceListLimit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25

  responses:
    AcceptedJob:
      description: Job accepted
      headers:
        X-Request-Id:
          required: true
          schema:
            type: string
        X-Trace-Id:
          $ref: "#/components/headers/XTraceId"
        Idempotency-Replayed:
          description: Present with value `true` when this response returns an earlier matching Job.
          schema:
            type: string
            const: "true"
        RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        RateLimit-Policy:
          $ref: "#/components/headers/RateLimitPolicy"
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/XRateLimitReset"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/DataJob"
    ScrapeEstimate:
      description: Maximum reservation estimate under the current pricing policy
      headers:
        X-Request-Id:
          required: true
          schema:
            type: string
        X-Trace-Id:
          $ref: "#/components/headers/XTraceId"
        RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        RateLimit-Policy:
          $ref: "#/components/headers/RateLimitPolicy"
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/XRateLimitReset"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/DataScrapeUsageEstimate"
    PaymentCheckoutError:
      description: Controlled console checkout failure
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PaymentCheckoutErrorEnvelope"
    PaymentCheckoutRateLimited:
      description: Checkout requests were sent too quickly
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PaymentCheckoutRateLimitEnvelope"
    PaymentCheckoutStatusError:
      description: Private checkout-status request failure
      headers:
        Cache-Control:
          $ref: "#/components/headers/PrivateNoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PaymentCheckoutErrorEnvelope"
    PaymentCheckoutStatusRateLimited:
      description: Checkout-status requests were sent too quickly
      headers:
        Cache-Control:
          $ref: "#/components/headers/PrivateNoStore"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PaymentCheckoutRateLimitEnvelope"
    PaymentCheckoutRecoveryError:
      description: Private checkout-recovery request failure
      headers:
        Cache-Control:
          $ref: "#/components/headers/PrivateNoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PaymentCheckoutErrorEnvelope"
    PaymentCheckoutRecoveryRateLimited:
      description: Checkout-recovery requests were sent too quickly
      headers:
        Cache-Control:
          $ref: "#/components/headers/PrivateNoStore"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PaymentCheckoutRateLimitEnvelope"
    BadRequest:
      description: Invalid request or blocked destination
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    Unauthenticated:
      description: Missing or invalid credentials
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    Forbidden:
      description: The credentials lack access or the request fails an activation or same-origin policy
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    InsufficientCredits:
      description: Insufficient available credits
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    NotFound:
      description: Resource not found in the authenticated tenant
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    Conflict:
      description: State or idempotency conflict
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    RateLimited:
      description: Rate or concurrency limit exceeded
      headers:
        RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        RateLimit-Policy:
          $ref: "#/components/headers/RateLimitPolicy"
        X-RateLimit-Limit:
          $ref: "#/components/headers/XRateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/XRateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/XRateLimitReset"
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    PayloadTooLarge:
      description: Request body exceeds the accepted byte limit
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    UsageLedgerTooLarge:
      description: |
        The deprecated unpaginated usage compatibility view exceeds its
        bounded ledger-line limit. The problem code is `LIMIT_EXCEEDED`; use
        the usage summary and cursor-paginated items operations instead.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    UnsupportedMediaType:
      description: Request body is not application/json
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    Gone:
      description: Resource retention has expired
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"
    ServiceUnavailable:
      description: A required storage, encryption, or rate-limit dependency is unavailable
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/PublicApiProblem"

  schemas:
    Workspace:
      type: object
      additionalProperties: false
      required:
        - id
        - kind
        - name
        - status
        - role
        - version
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^ws_[0-9a-f]{32}$"
        kind:
          type: string
          enum: [personal, team]
        name:
          type: string
          minLength: 1
          maxLength: 120
        status:
          type: string
          enum: [active, suspended, closed]
        role:
          type: string
          enum: [owner, admin, member]
        version:
          type: integer
          minimum: 1
          maximum: 1000000
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    WorkspaceEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Workspace"

    WorkspacePage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Workspace"
        meta:
          type: object
          additionalProperties: false
          required: [next_cursor]
          properties:
            next_cursor:
              type: "null"

    CreateTeamWorkspaceInput:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
          pattern: '^[^\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]{1,120}$'
          description: Trimmed display name without control or bidirectional-control characters.

    RenameTeamWorkspaceInput:
      type: object
      additionalProperties: false
      required: [name, expected_version]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
          pattern: '^[^\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]{1,120}$'
          description: Trimmed display name without control or bidirectional-control characters.
        expected_version:
          type: integer
          minimum: 1
          maximum: 1000000

    WorkspaceInvitation:
      type: object
      additionalProperties: false
      required:
        - id
        - workspace_id
        - role
        - status
        - expires_at
        - accepted_at
        - revoked_at
        - version
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^wsi_[0-9a-f]{32}$"
        workspace_id:
          type: string
          pattern: "^ws_[0-9a-f]{32}$"
        role:
          type: string
          enum: [admin, member]
        status:
          type: string
          enum: [pending, accepted, revoked, expired]
        expires_at:
          type: string
          format: date-time
        accepted_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        revoked_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        version:
          type: integer
          minimum: 1
          maximum: 1000000
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    WorkspaceInvitationEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/WorkspaceInvitation"

    WorkspaceInvitationPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WorkspaceInvitation"
        meta:
          $ref: "#/components/schemas/PageMeta"

    CreateWorkspaceInvitationInput:
      type: object
      additionalProperties: false
      required: [email]
      properties:
        email:
          type: string
          format: email
          maxLength: 320
        role:
          type: string
          enum: [admin, member]
          default: member
        expires_in_days:
          type: integer
          minimum: 1
          maximum: 7
          default: 7

    CreatedWorkspaceInvitation:
      type: object
      additionalProperties: false
      required: [invitation, email, token, delivery]
      properties:
        invitation:
          $ref: "#/components/schemas/WorkspaceInvitation"
        email:
          type: string
          format: email
          maxLength: 320
        token:
          type: string
          pattern: "^[A-Za-z0-9_-]{43}$"
          description: |
            One-time plaintext acceptance token. It is returned only by the
            successful create response and cannot be recovered by list APIs.
        delivery:
          $ref: "#/components/schemas/WorkspaceInvitationEmailDelivery"

    WorkspaceInvitationEmailDelivery:
      type: object
      additionalProperties: false
      required: [status, provider]
      properties:
        status:
          type: string
          enum: [sent, not_configured, failed]
          description: |
            Outcome of the single bounded best-effort send attempted after the
            invitation transaction committed. Every status is returned with
            HTTP 201 and does not change invitation validity.
        provider:
          type: [string, "null"]
          enum: [resend, cloudflare, null]
          description: Selected email provider, or null when none could be selected.
        error_code:
          type: string
          enum:
            - EMAIL_CONFIGURATION_UNAVAILABLE
            - EMAIL_APP_URL_INVALID
            - EMAIL_PROVIDER_UNSUPPORTED
            - EMAIL_PROVIDER_INCOMPLETE
            - EMAIL_DELIVERY_REJECTED
            - EMAIL_DELIVERY_FAILED
            - EMAIL_DELIVERY_TIMEOUT
          description: Bounded safe failure reason. Provider error text is never returned.

    CreatedWorkspaceInvitationEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/CreatedWorkspaceInvitation"

    AcceptWorkspaceInvitationInput:
      type: object
      additionalProperties: false
      required: [token]
      properties:
        token:
          type: string
          pattern: "^[A-Za-z0-9_-]{43}$"

    WorkspaceMember:
      type: object
      additionalProperties: false
      required:
        - id
        - user
        - role
        - status
        - version
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^wm_[0-9a-f]{32}$"
        user:
          type: object
          additionalProperties: false
          required: [name, email]
          properties:
            name:
              type: string
            email:
              type: string
              format: email
              maxLength: 320
        role:
          type: string
          enum: [owner, admin, member]
        status:
          type: string
          enum: [active, suspended]
        version:
          type: integer
          minimum: 1
          maximum: 1000000
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    WorkspaceMemberEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/WorkspaceMember"

    WorkspaceMemberPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WorkspaceMember"
        meta:
          $ref: "#/components/schemas/PageMeta"

    UpdateWorkspaceMemberInput:
      oneOf:
        - $ref: "#/components/schemas/UpdateWorkspaceMemberRoleInput"
        - $ref: "#/components/schemas/TransferWorkspaceOwnershipInput"

    UpdateWorkspaceMemberRoleInput:
      type: object
      additionalProperties: false
      required: [role, expected_version]
      properties:
        role:
          type: string
          enum: [admin, member]
        expected_version:
          type: integer
          minimum: 1
          maximum: 1000000

    TransferWorkspaceOwnershipInput:
      type: object
      additionalProperties: false
      required: [role, expected_version, expected_workspace_version]
      properties:
        role:
          type: string
          const: owner
        expected_version:
          type: integer
          minimum: 1
          maximum: 1000000
        expected_workspace_version:
          type: integer
          minimum: 1
          maximum: 1000000

    CommercialFulfillmentKind:
      type: string
      enum: [contract, credit_pack]
      description: |
        Server-authoritative fulfillment behavior frozen by the immutable
        commercial product version. `contract` may project a subscription or
        one-time lifetime entitlement. `credit_pack` grants only the paid
        order's credits and never changes the commercial-contract slot,
        workspace entitlement, or workspace limits.

    CommercialPaymentType:
      type: string
      enum: [subscription, one-time]

    CreditRecoveryCapability:
      oneOf:
        - type: object
          additionalProperties: false
          required: [available, reason]
          properties:
            available:
              type: boolean
              const: true
            reason:
              type: string
              const: available
        - type: object
          additionalProperties: false
          required: [available, reason]
          properties:
            available:
              type: boolean
              const: false
            reason:
              type: string
              enum:
                - catalog_unavailable
                - payment_provider_unavailable
                - temporarily_unavailable
      description: |
        Low-sensitivity, fail-closed self-service credit recovery capability.
        `catalog_unavailable` combines a missing, future-only, or currently
        disabled credit-pack catalog. `payment_provider_unavailable` means no
        provider is currently admitted for checkout with compatible pack
        money terms. Internal dependency failures collapse to
        `temporarily_unavailable`.

    PublicRuntimeConfig:
      type: object
      additionalProperties: false
      required: [credit_recovery]
      properties:
        app_name:
          type: string
        app_description:
          type: string
        app_logo:
          type: string
        email_auth_enabled:
          type: string
          enum: ["true", "false"]
        google_auth_enabled:
          type: string
          enum: ["true", "false"]
        github_auth_enabled:
          type: string
          enum: ["true", "false"]
        invite_code_required:
          type: string
          enum: ["true", "false"]
        select_payment_enabled:
          type: string
          enum: ["true", "false"]
        default_payment_provider:
          type: string
        stripe_enabled:
          type: string
          enum: ["true", "false"]
        creem_enabled:
          type: string
          enum: ["true", "false"]
        paypal_enabled:
          type: string
          enum: ["true", "false"]
        alipay_enabled:
          type: string
          enum: ["true", "false"]
        wechat_enabled:
          type: string
          enum: ["true", "false"]
        password_reset_enabled:
          type: string
          enum: ["true", "false"]
        email_verification_enabled:
          type: string
          enum: ["true", "false"]
        credit_recovery:
          $ref: "#/components/schemas/CreditRecoveryCapability"

    PublicRuntimeConfigEnvelope:
      type: object
      additionalProperties: false
      required: [code, message, data]
      properties:
        code:
          type: integer
          const: 0
        message:
          type: string
          const: ok
        data:
          $ref: "#/components/schemas/PublicRuntimeConfig"

    CommercialProductLimits:
      type: object
      additionalProperties: false
      required:
        - max_queued_jobs
        - max_active_jobs
        - max_http_concurrency
        - max_browser_concurrency
        - max_stealth_concurrency
        - retention_days
      properties:
        max_queued_jobs:
          type: integer
          minimum: 0
          maximum: 100000
        max_active_jobs:
          type: integer
          minimum: 1
          maximum: 10000
        max_http_concurrency:
          type: integer
          minimum: 1
          maximum: 10000
        max_browser_concurrency:
          type: integer
          minimum: 1
          maximum: 10000
        max_stealth_concurrency:
          type: integer
          minimum: 1
          maximum: 10000
        retention_days:
          type: integer
          minimum: 1
          maximum: 3650

    PublicCommercialProduct:
      type: object
      additionalProperties: false
      required:
        - id
        - product_key
        - plan_key
        - fulfillment_kind
        - payment_type
        - product_name
        - plan_name
        - description
        - price_minor
        - currency
        - currency_exponent
        - credits
        - credits_valid_days
        - interval
        - interval_count
        - limits
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 128
          description: Immutable commercial product-version identifier.
        product_key:
          type: string
          pattern: "^[a-z][a-z0-9_]{2,63}$"
        plan_key:
          type: string
          enum: [free, starter, pro, enterprise, operator]
        fulfillment_kind:
          $ref: "#/components/schemas/CommercialFulfillmentKind"
        payment_type:
          $ref: "#/components/schemas/CommercialPaymentType"
        product_name:
          type: string
          minLength: 1
          maxLength: 120
        plan_name:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type: string
          minLength: 1
          maxLength: 500
        price_minor:
          type: integer
          minimum: 1
          maximum: 2000000000
        currency:
          type: string
          pattern: "^[a-z]{3}$"
        currency_exponent:
          type: integer
          minimum: 0
          maximum: 3
        credits:
          type: integer
          minimum: 1
          maximum: 2000000000
        credits_valid_days:
          type: [integer, "null"]
          minimum: 1
          maximum: 3650
        interval:
          type: [string, "null"]
          enum: [month, year, null]
        interval_count:
          type: [integer, "null"]
          minimum: 1
          maximum: 12
        limits:
          allOf:
            - $ref: "#/components/schemas/CommercialProductLimits"
          description: |
            Plan limits associated with the catalog row. For
            `fulfillment_kind=credit_pack`, these remain compatibility
            metadata and are not projected by checkout settlement.

    AdminCommercialProductVersion:
      type: object
      additionalProperties: false
      required:
        - id
        - product_key
        - version
        - enabled
        - plan_version_id
        - plan_key
        - fulfillment_kind
        - payment_type
        - product_name
        - plan_name
        - description
        - price_minor
        - currency
        - currency_exponent
        - credits
        - credits_valid_days
        - interval
        - interval_count
        - effective_at
        - operator_note
        - created_at
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 128
        product_key:
          type: string
          pattern: "^[a-z][a-z0-9_]{2,63}$"
        version:
          type: integer
          minimum: 1
        enabled:
          type: boolean
        plan_version_id:
          type: string
          pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$"
          description: |
            Required by the current catalog for every product. For a credit
            pack it is compatibility metadata and must not be projected onto
            the purchasing workspace.
        plan_key:
          type: string
          enum: [free, starter, pro, enterprise, operator]
        fulfillment_kind:
          $ref: "#/components/schemas/CommercialFulfillmentKind"
        payment_type:
          $ref: "#/components/schemas/CommercialPaymentType"
        product_name:
          type: string
          minLength: 1
          maxLength: 120
        plan_name:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type: string
          minLength: 1
          maxLength: 500
        price_minor:
          type: integer
          minimum: 1
          maximum: 2000000000
        currency:
          type: string
          pattern: "^[a-z]{3}$"
        currency_exponent:
          type: integer
          minimum: 0
          maximum: 3
        credits:
          type: integer
          minimum: 1
          maximum: 2000000000
        credits_valid_days:
          type: [integer, "null"]
          minimum: 1
          maximum: 3650
        interval:
          type: [string, "null"]
          enum: [month, year, null]
        interval_count:
          type: [integer, "null"]
          minimum: 1
          maximum: 12
        effective_at:
          type: string
          format: date-time
        operator_note:
          type: [string, "null"]
          maxLength: 1000
        created_at:
          type: string
          format: date-time

    CreateCommercialProductVersionInput:
      type: object
      additionalProperties: false
      required:
        - enabled
        - plan_version_id
        - fulfillment_kind
        - payment_type
        - product_name
        - plan_name
        - description
        - price_minor
        - currency
        - currency_exponent
        - credits
        - effective_at
        - operator_note
      properties:
        enabled:
          type: boolean
        plan_version_id:
          type: string
          pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$"
          description: |
            Still required for a credit pack for catalog compatibility; it
            does not authorize entitlement or limit projection.
        fulfillment_kind:
          $ref: "#/components/schemas/CommercialFulfillmentKind"
        payment_type:
          $ref: "#/components/schemas/CommercialPaymentType"
        product_name:
          type: string
          minLength: 1
          maxLength: 120
        plan_name:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type: string
          minLength: 1
          maxLength: 500
        price_minor:
          type: integer
          minimum: 1
          maximum: 2000000000
        currency:
          type: string
          pattern: "^[a-z]{3}$"
        currency_exponent:
          type: integer
          minimum: 0
          maximum: 3
        credits:
          type: integer
          minimum: 1
          maximum: 2000000000
        credits_valid_days:
          type: [integer, "null"]
          minimum: 1
          maximum: 3650
        interval:
          type: [string, "null"]
          enum: [month, year, null]
        interval_count:
          type: [integer, "null"]
          minimum: 1
          maximum: 12
        effective_at:
          type: string
          format: date-time
        operator_note:
          type: string
          minLength: 20
          maxLength: 1000
      allOf:
        - if:
            properties:
              fulfillment_kind:
                const: credit_pack
          then:
            properties:
              payment_type:
                const: one-time
              interval:
                type: "null"
              interval_count:
                type: "null"
        - if:
            properties:
              payment_type:
                const: subscription
          then:
            required: [interval, interval_count]
            properties:
              fulfillment_kind:
                const: contract
              interval:
                type: string
                enum: [month, year]
              interval_count:
                type: integer
                minimum: 1
                maximum: 12
        - if:
            properties:
              payment_type:
                const: one-time
          then:
            properties:
              interval:
                type: "null"
              interval_count:
                type: "null"

    PublicCommercialProductEnvelope:
      type: object
      additionalProperties: false
      required: [code, message, data]
      properties:
        code:
          type: integer
          const: 0
        message:
          type: string
          const: ok
        data:
          type: array
          maxItems: 200
          items:
            $ref: "#/components/schemas/PublicCommercialProduct"

    AdminCommercialProductVersionEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/AdminCommercialProductVersion"

    CreatePaymentCheckoutRequest:
      type: object
      additionalProperties: false
      required: [product_id, product_version_id]
      properties:
        product_id:
          type: string
          minLength: 1
          maxLength: 128
          description: |
            Server catalog product-family key.
        product_version_id:
          type: string
          minLength: 3
          maxLength: 128
          pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$"
          description: |
            Immutable version displayed with the product. It must still be the
            effective version for product_id; stale pages receive 409 instead
            of being silently repriced.
        payment_provider:
          type: string
          enum: [stripe, creem, paypal, alipay, wechat]
        redirect:
          type: string
          maxLength: 2048
          description: Optional same-origin path; cross-origin values are discarded.

    PaymentCheckoutEnvelope:
      type: object
      additionalProperties: false
      required: [code, message, data]
      properties:
        code:
          type: integer
          const: 0
        message:
          type: string
          const: ok
        data:
          type: object
          additionalProperties: false
          required: [checkout_url, operation_id]
          properties:
            checkout_url:
              type: string
              format: uri-reference
              pattern: "^(https://|/payment/wechat-qr(?:[?#]|$))"
              maxLength: 8192
              description: HTTPS provider URL or the controlled same-origin WeChat QR path.
            operation_id:
              $ref: "#/components/schemas/PaymentCheckoutOperationId"

    PaymentCheckoutOperationId:
      type: string
      minLength: 1
      maxLength: 128
      pattern: "^[A-Za-z0-9_-]+$"
      description: |
        Durable user-owned checkout order identifier. It is correlation data,
        not a credential or evidence of payment.

    PaymentCheckoutRecoveryDiscoveryEnvelope:
      type: object
      additionalProperties: false
      required: [code, message, data]
      properties:
        code:
          type: integer
          const: 0
        message:
          type: string
          const: ok
        data:
          oneOf:
            - type: "null"
            - $ref: "#/components/schemas/PaymentCheckoutRecoveryDiscovery"

    PaymentCheckoutRecoveryDiscovery:
      type: object
      additionalProperties: false
      required: [operation_id, fulfillment_kind, state, can_resume]
      properties:
        operation_id:
          $ref: "#/components/schemas/PaymentCheckoutOperationId"
        fulfillment_kind:
          $ref: "#/components/schemas/CommercialFulfillmentKind"
        state:
          type: string
          enum:
            - creating_checkout
            - awaiting_payment
            - recovery_required
          description: Non-terminal state of the authenticated user's open checkout.
        can_resume:
          type: boolean
          const: true
          description: |
            The existing POST recovery path may resume this operation. An active
            attempt lease may still return 409 with Retry-After.

    PaymentCheckoutStatusRequest:
      type: object
      additionalProperties: false
      required: [operation_id]
      properties:
        operation_id:
          $ref: "#/components/schemas/PaymentCheckoutOperationId"

    PaymentCheckoutStatusEnvelope:
      type: object
      additionalProperties: false
      required: [code, message, data]
      properties:
        code:
          type: integer
          const: 0
        message:
          type: string
          const: ok
        data:
          type: object
          additionalProperties: false
          required:
            - operation_id
            - state
            - terminal
            - can_resume
            - payment_type
            - paid_at
            - updated_at
          properties:
            operation_id:
              $ref: "#/components/schemas/PaymentCheckoutOperationId"
            state:
              type: string
              enum:
                - creating_checkout
                - awaiting_payment
                - recovery_required
                - failed
                - succeeded
              description: |
                `recovery_required` is non-terminal and permits recovery;
                `failed` and `succeeded` are terminal.
            terminal:
              type: boolean
            can_resume:
              type: boolean
              description: True only when the frozen checkout may be recovered.
            payment_type:
              type: [string, "null"]
              maxLength: 32
            paid_at:
              type: [string, "null"]
              format: date-time
            updated_at:
              type: string
              format: date-time

    CurrentCommercialContractEnvelope:
      type: object
      additionalProperties: false
      required: [code, message, data]
      properties:
        code:
          type: integer
          const: 0
        message:
          type: string
          const: ok
        data:
          oneOf:
            - type: "null"
            - $ref: "#/components/schemas/CurrentCommercialContract"

    CurrentCommercialContract:
      type: object
      additionalProperties: false
      required:
        - state
        - source
        - status
        - planVersionId
        - accessEndsAt
        - graceEndsAt
      properties:
        state:
          type: string
          enum: [available, checkout_pending, subscription, lifetime, operator]
        source:
          type: string
          enum: [free, trial, subscription, lifetime, operator]
        status:
          type: string
          enum:
            [active, trialing, pending_cancel, grace, canceled, paused, expired]
        planVersionId:
          type: string
          minLength: 1
          maxLength: 128
        accessEndsAt:
          type: [string, "null"]
          format: date-time
        graceEndsAt:
          type: [string, "null"]
          format: date-time

    PaymentCheckoutErrorEnvelope:
      type: object
      additionalProperties: false
      required: [code, message]
      properties:
        code:
          type: integer
          const: -1
        message:
          type: string
          minLength: 1
          maxLength: 512
        data:
          type: object
          additionalProperties: true
          properties:
            error_code:
              type: string
              enum:
                - INVALID_REQUEST
                - UNAUTHENTICATED
                - FORBIDDEN
                - NOT_FOUND
                - CONFLICT
                - RATE_LIMITED
                - SERVICE_UNAVAILABLE
                - INTERNAL_ERROR
                - LIMIT_EXCEEDED
            reason:
              type: string
              maxLength: 128
            management_path:
              type: string
              enum: [/settings/billing, /settings/credits]
              description: |
                For `checkout_in_progress`, a frozen `credit_pack` order uses
                `/settings/credits`; a frozen `contract` order uses
                `/settings/billing`.

    PaymentCheckoutRateLimitEnvelope:
      type: object
      additionalProperties: false
      required: [error, message]
      properties:
        error:
          type: string
          const: too_many_requests
        message:
          type: string
          minLength: 1
          maxLength: 128

    DataExtractionSchema:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/ExtractionSchema"

    DataExtractionSchemaVersion:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/ExtractionSchemaVersion"

    ExtractionSchemaPage:
      type: object
      additionalProperties: false
      required: [data, pagination]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ExtractionSchema"
        pagination:
          type: object
          additionalProperties: false
          required: [next_cursor]
          properties:
            next_cursor:
              type: [string, "null"]
              maxLength: 512

    ExtractionSchemaVersionPage:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [items, next_before_version]
          properties:
            items:
              type: array
              items:
                $ref: "#/components/schemas/ExtractionSchemaVersion"
            next_before_version:
              type: [integer, "null"]
              minimum: 2
              maximum: 1000001

    CreateExtractionSchemaRequest:
      type: object
      additionalProperties: false
      required: [name, fields]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        fields:
          $ref: "#/components/schemas/SavedExtractionFields"

    UpdateExtractionSchemaRequest:
      type: object
      additionalProperties: false
      required: [expected_version]
      minProperties: 2
      properties:
        expected_version:
          type: integer
          minimum: 1
          maximum: 1000000
        name:
          type: string
          minLength: 1
          maxLength: 120
        status:
          type: string
          enum: [active, archived]

    CreateExtractionSchemaVersionRequest:
      type: object
      additionalProperties: false
      required: [expected_latest_version, fields]
      properties:
        expected_latest_version:
          type: integer
          minimum: 1
          maximum: 1000000
        fields:
          $ref: "#/components/schemas/SavedExtractionFields"

    MoveExtractionSchemaVersionRequest:
      type: object
      additionalProperties: false
      required: [version]
      properties:
        version:
          type: integer
          minimum: 1
          maximum: 1000000

    ExtractionSchema:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - status
        - latest_version
        - published_version
        - has_unpublished_changes
        - version
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^xs_[0-9a-f]{32}$"
        name:
          type: string
          minLength: 1
          maxLength: 120
        status:
          type: string
          enum: [active, archived]
        latest_version:
          type: integer
          minimum: 1
          maximum: 1000000
        published_version:
          type: [integer, "null"]
          minimum: 1
          maximum: 1000000
        has_unpublished_changes:
          type: boolean
        version:
          type: integer
          minimum: 1
          maximum: 1000000
          description: Optimistic metadata revision, independent of immutable content versions.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ExtractionSchemaVersion:
      type: object
      additionalProperties: false
      required: [schema_id, version, state, fields, content_hash, created_at]
      properties:
        schema_id:
          type: string
          pattern: "^xs_[0-9a-f]{32}$"
        version:
          type: integer
          minimum: 1
          maximum: 1000000
        state:
          type: string
          enum: [draft, published, historical]
        fields:
          $ref: "#/components/schemas/SavedExtractionFields"
        content_hash:
          type: string
          pattern: "^[0-9a-f]{64}$"
        created_at:
          type: string
          format: date-time

    SavedExtractionFields:
      type: array
      minItems: 1
      maxItems: 100
      description: |
        Unique named selectors; serialized JSON is limited to 256 KiB.
        Adaptive fields are limited to single-value CSS or XPath selectors.
      items:
        $ref: "#/components/schemas/ExtractionField"

    AccountDeletionRequest:
      type: object
      additionalProperties: false
      required: [confirmation]
      properties:
        confirmation:
          type: string
          minLength: 1
          maxLength: 512
          description: Exact `DELETE <current account email>` phrase.

    AccountDeletionEnvelope:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/AccountDeletion"

    AccountDeletion:
      type: object
      additionalProperties: false
      required:
        - id
        - state
        - access_revoked_at
        - requested_at
        - completed_at
        - manual_review_required
        - pending_object_count
      properties:
        id:
          type: string
          pattern: "^adr_[0-9a-f]{32}$"
        state:
          type: string
          enum: [purging, manual_review, completed]
        access_revoked_at:
          type: string
          format: date-time
        requested_at:
          type: string
          format: date-time
        completed_at:
          oneOf:
            - type: string
              format: date-time
            - type: "null"
        manual_review_required:
          type: boolean
        pending_object_count:
          type: integer
          minimum: 0

    DataJob:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Job"

    DataScrapeUsageEstimate:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/ScrapeUsageEstimate"

    ScrapeUsageEstimate:
      type: object
      additionalProperties: false
      required: [billing, items, assumptions, unpriced_meters]
      properties:
        billing:
          type: object
          additionalProperties: false
          required:
            - mode
            - pricing_version
            - estimate_kind
            - maximum_reserved_credits
            - actual_charges_may_be_lower
          properties:
            mode:
              type: string
              enum: [prepaid, unmetered]
            pricing_version:
              type: string
              minLength: 1
              maxLength: 128
            estimate_kind:
              type: string
              const: maximum_reservation
            maximum_reserved_credits:
              type: string
              pattern: "^[0-9]+\\.[0-9]{4}$"
            actual_charges_may_be_lower:
              type: boolean
        items:
          type: array
          minItems: 8
          maxItems: 8
          items:
            type: object
            additionalProperties: false
            required:
              - meter
              - unit
              - maximum_quantity
              - unit_credits
              - maximum_credits
            properties:
              meter:
                type: string
                enum:
                  - fetch_attempt
                  - page_success
                  - browser_second
                  - stealth_second
                  - download_mib
                  - proxy_mib
                  - artifact_storage
                  - webhook_delivery
              unit:
                type: string
                enum: [attempt, page, second, mebibyte, gibibyte_day, delivery]
              maximum_quantity:
                type: integer
                minimum: 0
                maximum: 100000
              unit_credits:
                type: string
                pattern: "^[0-9]+\\.[0-9]{4}$"
              maximum_credits:
                type: string
                pattern: "^[0-9]+\\.[0-9]{4}$"
        assumptions:
          type: object
          additionalProperties: false
          required:
            - job_attempt_limit
            - maximum_fetch_attempts_per_result
            - maximum_billable_fetch_attempts
            - maximum_page_successes
            - maximum_browser_seconds
            - maximum_stealth_seconds
            - maximum_download_mib
            - maximum_proxy_mib
            - maximum_artifact_storage_gib_days
            - maximum_webhook_deliveries
          properties:
            job_attempt_limit:
              type: integer
              minimum: 1
              maximum: 500
            maximum_fetch_attempts_per_result:
              type: integer
              const: 5
            maximum_billable_fetch_attempts:
              type: integer
              minimum: 1
              maximum: 2500
            maximum_page_successes:
              type: integer
              minimum: 1
              maximum: 100
            maximum_browser_seconds:
              type: integer
              minimum: 0
              maximum: 100000
            maximum_stealth_seconds:
              type: integer
              minimum: 0
              maximum: 100000
            maximum_download_mib:
              type: integer
              minimum: 0
              maximum: 100000
            maximum_proxy_mib:
              type: integer
              minimum: 0
              maximum: 100000
            maximum_artifact_storage_gib_days:
              type: integer
              minimum: 0
              maximum: 100000
            maximum_webhook_deliveries:
              type: integer
              minimum: 0
              maximum: 1
        unpriced_meters:
          type: array
          uniqueItems: true
          maxItems: 0
          description: Raw evidence meters are not separate customer-charge meters.

    DataJobUsage:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/JobUsageLedger"

    DataJobUsageSummary:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/JobUsageSummary"

    DataBillingBalance:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/BillingBalance"

    BillingBalance:
      type: object
      additionalProperties: false
      required:
        - available_credits
        - active_reserved_credits
        - active_reservation_count
        - nearest_expiration
        - as_of
      properties:
        available_credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
          description: Credits currently available for a new job reservation.
        active_reserved_credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
          description: Maximum credits held by reservations that remain active.
        active_reservation_count:
          type: integer
          minimum: 0
          description: Number of tenant reservations that remain active.
        nearest_expiration:
          type: [string, "null"]
          format: date-time
          description: |
            Earliest finite expiry among currently available grant credits, or
            null when no available grant has a finite future expiry.
        as_of:
          type: string
          format: date-time
          description: Clock boundary used to evaluate availability and expiry.

    TargetCredentialStatus:
      type: string
      enum: [active, disabled]

    SecretKind:
      type: string
      enum: [authorization, action_value, header_value]

    CreateSecretRequest:
      type: object
      additionalProperties: false
      required: [name, kind, value]
      allOf:
        - if:
            required: [kind]
            properties:
              kind:
                const: header_value
          then:
            properties:
              header_name: {}
              target_origin: {}
            required: [header_name, target_origin]
          else:
            not:
              anyOf:
                - properties:
                    header_name: {}
                  required: [header_name]
                - properties:
                    target_origin: {}
                  required: [target_origin]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        kind:
          $ref: "#/components/schemas/SecretKind"
        header_name:
          type: string
          minLength: 1
          maxLength: 256
          pattern: "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$"
          description: |
            Required only for header_value. The normalized wire header name is
            immutable after creation and cannot be a platform-managed name.
        target_origin:
          type: string
          format: uri
          maxLength: 2048
          description: |
            Required only for header_value. Exact public HTTP(S) origin with no
            path, query, fragment, or credentials. The normalized binding is
            immutable after creation.
        value:
          type: string
          writeOnly: true
          minLength: 1
          maxLength: 8192
          description: |
            For authorization secrets, a complete scheme and credential value such as
            `Bearer opaque-token` or `Basic base64-value`. Action values are passed only
            to the referenced controlled fill action. Header values are materialized
            only for the stored header name and exact target origin.

    UpdateSecretRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        value:
          type: string
          writeOnly: true
          minLength: 1
          maxLength: 8192
        status:
          $ref: "#/components/schemas/TargetCredentialStatus"

    Secret:
      type: object
      additionalProperties: false
      required:
        [
          id,
          name,
          kind,
          header_name,
          target_origin,
          status,
          version,
          created_at,
          updated_at,
        ]
      properties:
        id:
          type: string
          pattern: "^sec_[0-9a-f]{32}$"
        name:
          type: string
          minLength: 1
          maxLength: 120
        kind:
          $ref: "#/components/schemas/SecretKind"
        header_name:
          type: [string, "null"]
          maxLength: 256
          description: Non-null safe metadata only for header_value; never contains its value.
        target_origin:
          type: [string, "null"]
          format: uri
          description: Non-null normalized exact origin only for header_value.
        status:
          $ref: "#/components/schemas/TargetCredentialStatus"
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DataSecret:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Secret"

    SecretPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Secret"
        meta:
          $ref: "#/components/schemas/PageMeta"

    CookieInput:
      type: object
      additionalProperties: false
      required: [name, value]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 256
          pattern: "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$"
        value:
          type: string
          writeOnly: true
          maxLength: 4096
        path:
          type: string
          pattern: "^/"
          maxLength: 2048
          default: /
        secure:
          type: boolean
          default: true
        http_only:
          type: boolean
          default: true
        same_site:
          type: string
          enum: [Strict, Lax, None]
          default: Lax

    CreateCookieJarRequest:
      type: object
      additionalProperties: false
      required: [name, origin, cookies]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        origin:
          type: string
          format: uri
          maxLength: 2048
          description: |
            Exact public HTTP(S) origin with no path, query, fragment, or credentials.
            Jobs may reference the jar only when their normalized target origin matches.
        cookies:
          type: array
          minItems: 1
          maxItems: 50
          items:
            $ref: "#/components/schemas/CookieInput"

    UpdateCookieJarRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        cookies:
          type: array
          minItems: 1
          maxItems: 50
          items:
            $ref: "#/components/schemas/CookieInput"
        status:
          $ref: "#/components/schemas/TargetCredentialStatus"

    CookieJar:
      type: object
      additionalProperties: false
      required:
        [
          id,
          name,
          origin,
          cookie_count,
          status,
          version,
          created_at,
          updated_at,
        ]
      properties:
        id:
          type: string
          pattern: "^cj_[0-9a-f]{32}$"
        name:
          type: string
          minLength: 1
          maxLength: 120
        origin:
          type: string
          format: uri
        cookie_count:
          type: integer
          minimum: 1
          maximum: 50
        status:
          $ref: "#/components/schemas/TargetCredentialStatus"
        version:
          type: integer
          minimum: 1
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DataCookieJar:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/CookieJar"

    CookieJarPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/CookieJar"
        meta:
          $ref: "#/components/schemas/PageMeta"

    BrowserSessionEngine:
      type: string
      enum: [browser, stealth]

    BrowserSessionState:
      type: string
      enum: [active, closing, closed, expired, failed]

    BrowserSessionAdmissionConflictDetails:
      type: object
      additionalProperties: false
      required: [conflict_kind, reason]
      properties:
        conflict_kind:
          type: string
          const: browser_session_admission_changed
        reason:
          type: string
          enum:
            - bound_resource_unavailable
            - engine_mismatch
            - expired
            - in_use
            - inactive
            - state_version_mismatch
        state:
          $ref: "#/components/schemas/BrowserSessionState"
        session_engine:
          $ref: "#/components/schemas/BrowserSessionEngine"
        current_state_version:
          type: integer
          minimum: 0
          maximum: 2147483647

    BrowserSessionAdmissionConflictProblem:
      allOf:
        - $ref: "#/components/schemas/PublicApiProblem"
        - type: object
          required: [details]
          properties:
            status:
              type: integer
              const: 409
            code:
              type: string
              const: CONFLICT
            details:
              $ref: "#/components/schemas/BrowserSessionAdmissionConflictDetails"

    CreateBrowserSessionRequest:
      type: object
      additionalProperties: false
      required: [name]
      description: |
        max_idle_seconds may not exceed ttl_seconds. storage_state is not an
        accepted client field; the server creates and encrypts an empty state.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        engine:
          allOf:
            - $ref: "#/components/schemas/BrowserSessionEngine"
          default: browser
        proxy_policy_id:
          type: [string, "null"]
          pattern: "^pp_[0-9a-f]{32}$"
          default: null
          description: |
            Reserved for sticky session-proxy execution. Non-null values
            currently fail closed with POLICY_DENIED; omit this field for
            direct-first execution.
        cookie_jar_id:
          type: [string, "null"]
          pattern: "^cj_[0-9a-f]{32}$"
          default: null
          description: Active same-workspace jar used only to seed state version zero.
        locale:
          type: string
          minLength: 2
          maxLength: 35
          default: en-US
        timezone:
          type: string
          minLength: 1
          maxLength: 64
          default: UTC
        ttl_seconds:
          type: integer
          minimum: 60
          maximum: 14400
          default: 1800
        max_idle_seconds:
          type: integer
          minimum: 60
          maximum: 14400
          default: 300

    BrowserSession:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - workspace_id
        - engine
        - state
        - proxy_policy_id
        - cookie_jar_id
        - locale
        - timezone
        - ttl_seconds
        - max_idle_seconds
        - profile_version
        - state_version
        - last_activity_at
        - expires_at
        - idle_expires_at
        - closed_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^ses_[0-9a-f]{32}$"
        name:
          type: string
          minLength: 1
          maxLength: 120
        workspace_id:
          type: string
          pattern: "^ws_[0-9a-f]{32}$"
          description: Public workspace identity; the internal UUID is never exposed.
        engine:
          $ref: "#/components/schemas/BrowserSessionEngine"
        state:
          $ref: "#/components/schemas/BrowserSessionState"
        proxy_policy_id:
          type: [string, "null"]
          pattern: "^pp_[0-9a-f]{32}$"
        cookie_jar_id:
          type: [string, "null"]
          pattern: "^cj_[0-9a-f]{32}$"
        locale:
          type: string
        timezone:
          type: string
        ttl_seconds:
          type: integer
          minimum: 60
          maximum: 14400
        max_idle_seconds:
          type: integer
          minimum: 60
          maximum: 14400
        profile_version:
          type: string
          const: desktop_chromium_v1
        state_version:
          type: integer
          minimum: 0
          maximum: 2147483647
          description: Optimistic fence required by nested scrape commands.
        last_activity_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        idle_expires_at:
          type: string
          format: date-time
        closed_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DataBrowserSession:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/BrowserSession"

    BrowserSessionPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/BrowserSession"
        meta:
          $ref: "#/components/schemas/PageMeta"

    ScheduleState:
      type: string
      enum: [active, paused]

    ScheduleKind:
      type: string
      enum: [scrape, batch, crawl]

    ScheduleRunState:
      type: string
      enum: [waiting, launching, submitted, skipped, failed]

    ScheduleJobOutcome:
      type: string
      enum: [cancelled, succeeded, partially_succeeded, failed, source_lost]

    CreateScheduleRequest:
      oneOf:
        - $ref: "#/components/schemas/CreateScrapeScheduleRequest"
        - $ref: "#/components/schemas/CreateBatchScheduleRequest"
        - $ref: "#/components/schemas/CreateCrawlScheduleRequest"
      description: |
        The workload shape is discriminated by `kind`. Scrape remains the
        backward-compatible default when `kind` is omitted; Batch and Crawl
        require their explicit immutable kind. Every branch uses the scheduled
        safe subset and rejects POST, persistent browser sessions, and
        side-effecting render actions.

    UpdateScheduleRequest:
      oneOf:
        - $ref: "#/components/schemas/UpdateScheduleWithoutKindRequest"
        - $ref: "#/components/schemas/UpdateScrapeScheduleRequest"
        - $ref: "#/components/schemas/UpdateBatchScheduleRequest"
        - $ref: "#/components/schemas/UpdateCrawlScheduleRequest"
      description: |
        `kind` is an optional immutable-kind assertion for metadata-only
        updates. A Batch or Crawl workload replacement requires its matching
        explicit kind; a request without `kind` can replace only a Scrape
        workload. Change detection is available only on Scrape schedules.

    ScheduleCreateFields:
      type: object
      required: [name, cron, spec]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        cron:
          type: string
          minLength: 9
          maxLength: 128
          pattern: "^[0-9*/,-]+\\s+[0-9*/,-]+\\s+[0-9*/,-]+\\s+[0-9*/,-]+\\s+[0-9*/,-]+$"
        timezone:
          type: string
          minLength: 1
          maxLength: 64
          default: UTC
          description: IANA timezone used to evaluate cron wall-clock slots and daylight-saving transitions.
        change_detection:
          $ref: "#/components/schemas/ScheduleChangeDetection"
        enabled:
          type: boolean
          default: true
          description: False creates the schedule paused without launching a slot.

    CreateScrapeScheduleRequest:
      allOf:
        - $ref: "#/components/schemas/ScheduleCreateFields"
        - type: object
          properties:
            kind:
              type: string
              const: scrape
              default: scrape
              description: May be omitted for the backward-compatible Scrape default.
            spec:
              $ref: "#/components/schemas/ScheduleSafeScrapeRequest"
      unevaluatedProperties: false

    CreateBatchScheduleRequest:
      allOf:
        - $ref: "#/components/schemas/ScheduleCreateFields"
        - type: object
          required: [kind]
          properties:
            kind:
              type: string
              const: batch
            spec:
              $ref: "#/components/schemas/ScheduleSafeBatchRequest"
            change_detection:
              $ref: "#/components/schemas/ScheduleDisabledChangeDetection"
      unevaluatedProperties: false

    CreateCrawlScheduleRequest:
      allOf:
        - $ref: "#/components/schemas/ScheduleCreateFields"
        - type: object
          required: [kind]
          properties:
            kind:
              type: string
              const: crawl
            spec:
              $ref: "#/components/schemas/ScheduleSafeCrawlRequest"
            change_detection:
              $ref: "#/components/schemas/ScheduleDisabledChangeDetection"
      unevaluatedProperties: false

    ScheduleUpdateFields:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        cron:
          type: string
          minLength: 9
          maxLength: 128
          pattern: "^[0-9*/,-]+\\s+[0-9*/,-]+\\s+[0-9*/,-]+\\s+[0-9*/,-]+\\s+[0-9*/,-]+$"
        timezone:
          type: string
          minLength: 1
          maxLength: 64
          description: IANA timezone used to evaluate future slots.
        change_detection:
          $ref: "#/components/schemas/ScheduleChangeDetection"

    UpdateScheduleWithoutKindRequest:
      allOf:
        - $ref: "#/components/schemas/ScheduleUpdateFields"
        - type: object
          properties:
            spec:
              $ref: "#/components/schemas/ScheduleSafeScrapeRequest"
      anyOf:
        - required: [name]
        - required: [cron]
        - required: [timezone]
        - required: [spec]
        - required: [change_detection]
      unevaluatedProperties: false

    UpdateScrapeScheduleRequest:
      allOf:
        - $ref: "#/components/schemas/ScheduleUpdateFields"
        - type: object
          required: [kind]
          properties:
            kind:
              type: string
              const: scrape
            spec:
              $ref: "#/components/schemas/ScheduleSafeScrapeRequest"
      anyOf:
        - required: [name]
        - required: [cron]
        - required: [timezone]
        - required: [spec]
        - required: [change_detection]
      unevaluatedProperties: false

    UpdateBatchScheduleRequest:
      allOf:
        - $ref: "#/components/schemas/ScheduleUpdateFields"
        - type: object
          required: [kind]
          properties:
            kind:
              type: string
              const: batch
            spec:
              $ref: "#/components/schemas/ScheduleSafeBatchRequest"
            change_detection:
              $ref: "#/components/schemas/ScheduleDisabledChangeDetection"
      anyOf:
        - required: [name]
        - required: [cron]
        - required: [timezone]
        - required: [spec]
        - required: [change_detection]
      unevaluatedProperties: false

    UpdateCrawlScheduleRequest:
      allOf:
        - $ref: "#/components/schemas/ScheduleUpdateFields"
        - type: object
          required: [kind]
          properties:
            kind:
              type: string
              const: crawl
            spec:
              $ref: "#/components/schemas/ScheduleSafeCrawlRequest"
            change_detection:
              $ref: "#/components/schemas/ScheduleDisabledChangeDetection"
      anyOf:
        - required: [name]
        - required: [cron]
        - required: [timezone]
        - required: [spec]
        - required: [change_detection]
      unevaluatedProperties: false

    ScheduleDisabledChangeDetection:
      allOf:
        - $ref: "#/components/schemas/ScheduleChangeDetection"
        - type: object
          properties:
            enabled:
              type: boolean
              const: false

    ScheduleChangeDetection:
      type: object
      additionalProperties: false
      required: [enabled]
      properties:
        enabled:
          type: boolean
          default: false
          description: |
            Requires a saved or inline structured extraction definition and is
            available only for single-page Scrape schedules. Scheduled Batch
            and Crawl v1 reject `true`.

    ScheduleRun:
      type: object
      additionalProperties: false
      required:
        - id
        - schedule_id
        - schedule_version
        - scheduled_for
        - state
        - job_id
        - job_outcome
        - job_finished_at
        - result_count
        - credits_charged
        - change
        - skip_reason
        - error_code
        - attempt_count
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^schrun_[0-9a-f]{32}$"
        schedule_id:
          type: string
          pattern: "^sch_[0-9a-f]{32}$"
        schedule_version:
          type: integer
          minimum: 1
        scheduled_for:
          type: string
          format: date-time
          description: Unique UTC cron slot for this schedule.
        state:
          $ref: "#/components/schemas/ScheduleRunState"
        job_id:
          oneOf:
            - $ref: "#/components/parameters/JobId/schema"
            - type: "null"
        job_outcome:
          oneOf:
            - $ref: "#/components/schemas/ScheduleJobOutcome"
            - type: "null"
          description: Frozen terminal job state, source_lost for a legacy row whose source was already missing at migration, or null while projection is pending.
        job_finished_at:
          type: [string, "null"]
          format: date-time
          description: Frozen terminal job completion time.
        result_count:
          type: [integer, "null"]
          minimum: 0
          description: Frozen final job item_count; retry-attempt rows are not counted.
        credits_charged:
          type: [string, "null"]
          pattern: "^[0-9]+\\.[0-9]{4}$"
          description: Frozen final charged credits using four decimal places.
        change:
          oneOf:
            - $ref: "#/components/schemas/ScheduleChange"
            - type: "null"
          description: Durable comparison outcome for a monitored run.
        skip_reason:
          oneOf:
            - type: string
              enum: [misfire, non_overlap]
            - type: "null"
          description: Bounded safe reason for a deliberately skipped slot.
        error_code:
          type: [string, "null"]
          pattern: "^[A-Z][A-Z0-9_]{2,63}$"
          description: Bounded safe admission or launch failure code.
        attempt_count:
          type: integer
          minimum: 0
          maximum: 8
          description: Number of idempotent launch attempts for this slot.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Schedule:
      type: object
      additionalProperties: false
      required:
        - id
        - kind
        - name
        - state
        - cron
        - timezone
        - spec
        - spec_version
        - change_detection
        - version
        - next_run_at
        - last_scheduled_at
        - latest_run
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^sch_[0-9a-f]{32}$"
        kind:
          $ref: "#/components/schemas/ScheduleKind"
        name:
          type: string
          minLength: 1
          maxLength: 120
        state:
          $ref: "#/components/schemas/ScheduleState"
        cron:
          type: string
          minLength: 9
          maxLength: 128
          pattern: "^[0-9*/,-]+\\s+[0-9*/,-]+\\s+[0-9*/,-]+\\s+[0-9*/,-]+\\s+[0-9*/,-]+$"
        timezone:
          type: string
          maxLength: 64
        spec:
          oneOf:
            - $ref: "#/components/schemas/ScheduledScrapeSpec"
            - $ref: "#/components/schemas/ScheduledBatchSpec"
            - $ref: "#/components/schemas/ScheduledCrawlSpec"
          description: Frozen normalized workload matching `kind`.
        spec_version:
          type: integer
          minimum: 1
          description: Immutable workload reader version frozen into each run.
        change_detection:
          $ref: "#/components/schemas/ScheduleChangeDetection"
        version:
          type: integer
          minimum: 1
          description: Optimistic schedule state and configuration fence.
        next_run_at:
          type: [string, "null"]
          format: date-time
          description: Next future UTC slot; null while paused or deleted.
        last_scheduled_at:
          type: [string, "null"]
          format: date-time
        latest_run:
          oneOf:
            - $ref: "#/components/schemas/ScheduleRun"
            - type: "null"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DataSchedule:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/Schedule"

    SchedulePage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Schedule"
        meta:
          $ref: "#/components/schemas/PageMeta"

    ScheduleRunPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ScheduleRun"
        meta:
          $ref: "#/components/schemas/PageMeta"

    ScheduleChangedPath:
      type: object
      additionalProperties: false
      required: [op, path]
      properties:
        op:
          type: string
          enum: [add, remove, replace]
        path:
          type: string
          description: RFC 6901 JSON Pointer. No extracted value is included.

    ScheduleChange:
      type: object
      additionalProperties: false
      required:
        - id
        - status
        - snapshot_id
        - snapshot_available
        - previous_snapshot_id
        - content_hash
        - previous_content_hash
        - changed_paths
        - diff_truncated
        - error_code
        - webhook_delivery_id
        - evaluated_at
      properties:
        id:
          type: string
          pattern: "^schg_[0-9a-f]{32}$"
        status:
          type: string
          enum: [baseline, unchanged, changed, unavailable]
        snapshot_id:
          type: [string, "null"]
          pattern: "^ssnap_[0-9a-f]{32}$"
        snapshot_available:
          type: boolean
        previous_snapshot_id:
          type: [string, "null"]
          pattern: "^ssnap_[0-9a-f]{32}$"
        content_hash:
          type: [string, "null"]
          pattern: "^[0-9a-f]{64}$"
        previous_content_hash:
          type: [string, "null"]
          pattern: "^[0-9a-f]{64}$"
        changed_paths:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/ScheduleChangedPath"
        diff_truncated:
          type: boolean
        error_code:
          type: [string, "null"]
          pattern: "^[A-Z][A-Z0-9_]{2,63}$"
        webhook_delivery_id:
          type: [string, "null"]
          pattern: "^whd_[0-9a-f]{32}$"
        evaluated_at:
          type: string
          format: date-time

    ScheduleSnapshot:
      type: object
      additionalProperties: false
      required:
        - id
        - schedule_id
        - run_id
        - source_job_id
        - source_result_id
        - content_hash
        - byte_size
        - content
        - expires_at
        - created_at
      properties:
        id:
          type: string
          pattern: "^ssnap_[0-9a-f]{32}$"
        schedule_id:
          type: string
          pattern: "^sch_[0-9a-f]{32}$"
        run_id:
          type: string
          pattern: "^schrun_[0-9a-f]{32}$"
        source_job_id:
          type: string
          pattern: "^job_[0-9a-f]{32}$"
        source_result_id:
          type: string
          pattern: "^res_[0-9a-f]{32}$"
        content_hash:
          type: string
          pattern: "^[0-9a-f]{64}$"
        byte_size:
          type: integer
          minimum: 1
          maximum: 65536
        content:
          type: object
          additionalProperties: true
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time

    DataScheduleSnapshot:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/ScheduleSnapshot"

    DataProxyPolicy:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/ProxyPolicy"

    ProxyPolicyPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ProxyPolicy"
        meta:
          $ref: "#/components/schemas/PageMeta"

    ProxyPolicyProvider:
      type: string
      enum: [webshare, byo]

    ProxyPoolKind:
      type: string
      enum: [direct, rotating, sticky]

    ProxyProtocol:
      type: string
      enum: [auto, http, socks5]

    ProxyPolicyStatus:
      type: string
      enum: [active, disabled]

    ProxyEndpointInput:
      type: object
      additionalProperties: false
      required: [url]
      description: |
        A host-and-port-only HTTP, HTTPS, or SOCKS5 URL. Credentials must use
        the separate fields and, when configured, username and password must
        both be non-empty.
      allOf:
        - oneOf:
            - properties:
                username:
                  type: "null"
                password:
                  type: "null"
            - required: [username, password]
              properties:
                username:
                  type: string
                  minLength: 1
                password:
                  type: string
                  minLength: 1
      properties:
        url:
          type: string
          minLength: 1
          maxLength: 2048
          pattern: "^(?:https?|socks5)://"
        username:
          type: [string, "null"]
          maxLength: 512
          default: null
        password:
          type: [string, "null"]
          maxLength: 1024
          default: null

    CreateProxyPolicyRequest:
      oneOf:
        - $ref: "#/components/schemas/CreateWebshareProxyPolicyRequest"
        - $ref: "#/components/schemas/CreateByoProxyPolicyRequest"
      discriminator:
        propertyName: provider
        mapping:
          webshare: "#/components/schemas/CreateWebshareProxyPolicyRequest"
          byo: "#/components/schemas/CreateByoProxyPolicyRequest"

    CreateWebshareProxyPolicyRequest:
      type: object
      additionalProperties: false
      required: [name, provider]
      allOf:
        - if:
            required: [pool_kind]
            properties:
              pool_kind:
                const: sticky
          then:
            required: [sticky_ttl_seconds]
            properties:
              sticky_ttl_seconds:
                type: integer
          else:
            properties:
              sticky_ttl_seconds:
                type: "null"
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        provider:
          type: string
          const: webshare
        pool_kind:
          $ref: "#/components/schemas/ProxyPoolKind"
          default: direct
        protocol:
          $ref: "#/components/schemas/ProxyProtocol"
          default: auto
        country_code:
          type: [string, "null"]
          pattern: "^[A-Za-z]{2}$"
          default: null
          description: ISO 3166-1 alpha-2 code, normalized to uppercase.
        sticky_ttl_seconds:
          type: [integer, "null"]
          minimum: 60
          maximum: 86400
          default: null
          description: Required only when pool_kind is sticky.
        max_concurrency:
          type: integer
          minimum: 1
          maximum: 1000
          default: 20

    CreateByoProxyPolicyRequest:
      type: object
      additionalProperties: false
      required: [name, provider, endpoint]
      allOf:
        - if:
            required: [pool_kind]
            properties:
              pool_kind:
                const: sticky
          then:
            required: [sticky_ttl_seconds]
            properties:
              sticky_ttl_seconds:
                type: integer
          else:
            properties:
              sticky_ttl_seconds:
                type: "null"
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        provider:
          type: string
          const: byo
        pool_kind:
          $ref: "#/components/schemas/ProxyPoolKind"
          default: direct
        protocol:
          $ref: "#/components/schemas/ProxyProtocol"
          default: auto
        country_code:
          type: [string, "null"]
          pattern: "^[A-Za-z]{2}$"
          default: null
          description: ISO 3166-1 alpha-2 code, normalized to uppercase.
        sticky_ttl_seconds:
          type: [integer, "null"]
          minimum: 60
          maximum: 86400
          default: null
          description: Required only when pool_kind is sticky.
        max_concurrency:
          type: integer
          minimum: 1
          maximum: 1000
          default: 20
        endpoint:
          $ref: "#/components/schemas/ProxyEndpointInput"

    UpdateProxyPolicyRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        protocol:
          $ref: "#/components/schemas/ProxyProtocol"
        country_code:
          type: [string, "null"]
          pattern: "^[A-Za-z]{2}$"
          description: ISO 3166-1 alpha-2 code, normalized to uppercase.
        sticky_ttl_seconds:
          type: [integer, "null"]
          minimum: 60
          maximum: 86400
        max_concurrency:
          type: integer
          minimum: 1
          maximum: 1000
        status:
          $ref: "#/components/schemas/ProxyPolicyStatus"
        endpoint:
          $ref: "#/components/schemas/ProxyEndpointInput"

    ProxyPolicy:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - provider
        - pool_kind
        - protocol
        - country_code
        - sticky_ttl_seconds
        - max_concurrency
        - status
        - credentials_configured
        - health
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^pp_[0-9a-f]{32}$"
        name:
          type: string
          minLength: 1
          maxLength: 120
        provider:
          $ref: "#/components/schemas/ProxyPolicyProvider"
        pool_kind:
          $ref: "#/components/schemas/ProxyPoolKind"
        protocol:
          $ref: "#/components/schemas/ProxyProtocol"
        country_code:
          type: [string, "null"]
          pattern: "^[A-Z]{2}$"
        sticky_ttl_seconds:
          type: [integer, "null"]
          minimum: 60
          maximum: 86400
        max_concurrency:
          type: integer
          minimum: 1
          maximum: 1000
        status:
          $ref: "#/components/schemas/ProxyPolicyStatus"
        credentials_configured:
          type: boolean
        health:
          type: object
          additionalProperties: false
          required: [available_endpoints, healthy_endpoints]
          properties:
            available_endpoints:
              type: integer
              minimum: 0
            healthy_endpoints:
              type: integer
              minimum: 0
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    WebhookEndpointVerificationEvent:
      type: object
      additionalProperties: false
      required: [data, id, type]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [challenge]
          properties:
            challenge:
              type: string
              pattern: "^whv_[A-Za-z0-9_-]{43}$"
        id:
          type: string
          pattern: "^evt_[0-9a-f]{32}$"
        type:
          type: string
          const: webhook.endpoint_verification

    JobCompletedWebhookEvent:
      type: object
      additionalProperties: false
      required: [created_at, data, id, type]
      properties:
        created_at:
          type: string
          format: date-time
        data:
          type: object
          additionalProperties: false
          required: [job]
          properties:
            job:
              $ref: "#/components/schemas/WebhookJobSummary"
        id:
          type: string
          pattern: "^evt_[0-9a-f]{32}$"
        type:
          type: string
          const: job.completed

    ScheduleChangeDetectedWebhookEvent:
      type: object
      additionalProperties: false
      required: [created_at, data, id, type]
      properties:
        created_at:
          type: string
          format: date-time
        data:
          type: object
          additionalProperties: false
          required:
            - schedule_id
            - run_id
            - change_id
            - snapshot_id
            - previous_snapshot_id
            - content_hash
            - previous_content_hash
            - changed_paths
            - diff_truncated
            - detected_at
          properties:
            schedule_id:
              type: string
              pattern: "^sch_[0-9a-f]{32}$"
            run_id:
              type: string
              pattern: "^schrun_[0-9a-f]{32}$"
            change_id:
              type: string
              pattern: "^schg_[0-9a-f]{32}$"
            snapshot_id:
              type: string
              pattern: "^ssnap_[0-9a-f]{32}$"
            previous_snapshot_id:
              type: string
              pattern: "^ssnap_[0-9a-f]{32}$"
            content_hash:
              type: string
              pattern: "^[0-9a-f]{64}$"
            previous_content_hash:
              type: string
              pattern: "^[0-9a-f]{64}$"
            changed_paths:
              type: array
              maxItems: 100
              items:
                $ref: "#/components/schemas/ScheduleChangedPath"
            diff_truncated:
              type: boolean
            detected_at:
              type: string
              format: date-time
        id:
          type: string
          pattern: "^evt_[0-9a-f]{32}$"
        type:
          type: string
          const: schedule.change_detected

    WebhookJobSummary:
      type: object
      additionalProperties: false
      required:
        - id
        - kind
        - state
        - progress
        - usage
        - error
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^job_[A-Za-z0-9]+$"
        kind:
          $ref: "#/components/schemas/JobKind"
        state:
          type: string
          enum: [cancelled, succeeded, partially_succeeded, failed]
        progress:
          $ref: "#/components/schemas/JobProgress"
        usage:
          type: object
          additionalProperties: false
          required: [attempts, download_bytes, browser_ms, proxy_bytes, credits]
          properties:
            attempts:
              type: integer
              minimum: 0
            download_bytes:
              type: integer
              minimum: 0
            browser_ms:
              type: integer
              minimum: 0
            proxy_bytes:
              type: integer
              minimum: 0
            credits:
              type: string
              pattern: "^[0-9]+\\.[0-9]{4}$"
        error:
          oneOf:
            - type: object
              additionalProperties: false
              required: [code, message]
              properties:
                code:
                  type: string
                  pattern: "^[A-Z][A-Z0-9_]{2,63}$"
                message:
                  type: string
                  minLength: 1
                  maxLength: 1000
            - type: "null"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    WebhookEndpointStatus:
      type: string
      enum: [pending_verification, active, disabled]

    CreateWebhookEndpointRequest:
      type: object
      additionalProperties: false
      required: [name, url]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        url:
          type: string
          format: uri
          pattern: "^https://"
          maxLength: 2048
        event_types:
          type: array
          minItems: 1
          maxItems: 2
          uniqueItems: true
          default: [job.completed, schedule.change_detected]
          items:
            type: string
            enum: [job.completed, schedule.change_detected]

    UpdateWebhookEndpointRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
        url:
          type: string
          format: uri
          pattern: "^https://"
          maxLength: 2048
        status:
          type: string
          const: disabled
        event_types:
          type: array
          minItems: 1
          maxItems: 2
          uniqueItems: true
          items:
            type: string
            enum: [job.completed, schedule.change_detected]

    WebhookEndpoint:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - url
        - event_types
        - status
        - verified_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^wh_[0-9a-f]{32}$"
        name:
          type: string
          minLength: 1
          maxLength: 120
        url:
          type: string
          format: uri
          pattern: "^https://"
          maxLength: 2048
        event_types:
          type: array
          minItems: 1
          maxItems: 2
          uniqueItems: true
          items:
            type: string
            enum: [job.completed, schedule.change_detected]
        status:
          $ref: "#/components/schemas/WebhookEndpointStatus"
        verified_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DataWebhookEndpoint:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/WebhookEndpoint"

    DataCreatedWebhookEndpoint:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [endpoint, signing_secret]
          properties:
            endpoint:
              $ref: "#/components/schemas/WebhookEndpoint"
            signing_secret:
              type: string
              writeOnly: true
              pattern: "^whsec_[A-Za-z0-9_-]{43}$"

    WebhookEndpointPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WebhookEndpoint"
        meta:
          $ref: "#/components/schemas/PageMeta"

    WebhookDeliveryState:
      type: string
      enum:
        [
          waiting,
          delivering,
          retry_scheduled,
          succeeded,
          dead_letter,
          cancelled,
        ]

    WebhookDelivery:
      type: object
      additionalProperties: false
      required:
        - id
        - event_id
        - endpoint
        - job_id
        - event_type
        - state
        - attempt_count
        - max_attempts
        - next_attempt_at
        - claim_expires_at
        - last_response_status
        - last_error_code
        - payload_available
        - succeeded_at
        - failed_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^whd_[0-9a-f]{32}$"
        event_id:
          type: string
          pattern: "^evt_[0-9a-f]{32}$"
        endpoint:
          type: object
          additionalProperties: false
          required: [id, name]
          properties:
            id:
              type: string
              pattern: "^wh_[0-9a-f]{32}$"
            name:
              type: string
              minLength: 1
              maxLength: 120
        job_id:
          type: string
          pattern: "^job_[0-9a-f]{32}$"
        event_type:
          type: string
          enum: [job.completed, schedule.change_detected]
        state:
          $ref: "#/components/schemas/WebhookDeliveryState"
        attempt_count:
          type: integer
          minimum: 0
          maximum: 20
        max_attempts:
          type: integer
          minimum: 1
          maximum: 20
        next_attempt_at:
          type: [string, "null"]
          format: date-time
        claim_expires_at:
          type: [string, "null"]
          format: date-time
        last_response_status:
          type: [integer, "null"]
          minimum: 100
          maximum: 599
        last_error_code:
          type: [string, "null"]
          pattern: "^[A-Z][A-Z0-9_]{2,63}$"
        payload_available:
          type: boolean
          description: False after terminal webhook body retention has expired; replay is then unavailable.
        succeeded_at:
          type: [string, "null"]
          format: date-time
        failed_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    DataWebhookDelivery:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/WebhookDelivery"

    WebhookDeliveryPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/WebhookDelivery"
        meta:
          $ref: "#/components/schemas/PageMeta"

    DataArtifactMetadata:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/ArtifactMetadata"

    CreateJobExportRequest:
      type: object
      additionalProperties: false
      required: [format]
      properties:
        format:
          type: string
          enum: [jsonl, csv]

    DataJobExport:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: "#/components/schemas/JobExport"

    JobExportList:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/JobExport"

    JobExport:
      type: object
      additionalProperties: false
      required:
        - id
        - job_id
        - format
        - selection
        - schema_version
        - generation
        - state
        - row_count
        - bytes
        - sha256
        - error
        - expires_at
        - created_at
        - updated_at
        - download_url
      description: |
        Tenant-scoped immutable export metadata. Object-store provider, bucket,
        staging key, final key, claim owner, and retry diagnostics are absent.
        download_url is non-null only while state is ready and retention has
        not expired.
      properties:
        id:
          type: string
          pattern: "^exp_[0-9a-f]{32}$"
        job_id:
          type: string
          pattern: "^job_[0-9a-f]{32}$"
        format:
          type: string
          enum: [jsonl, csv]
        selection:
          type: string
          const: final_request_results_v1
        schema_version:
          type: integer
          const: 1
        generation:
          type: integer
          minimum: 1
        state:
          type: string
          enum:
            - queued
            - generating
            - retry_scheduled
            - ready
            - failed
            - expired
        row_count:
          type: integer
          minimum: 0
          maximum: 100
          description: Frozen when the export is accepted, including while queued.
        bytes:
          type: [integer, "null"]
          minimum: 0
          maximum: 33554432
        sha256:
          type: [string, "null"]
          pattern: "^[a-f0-9]{64}$"
        error:
          oneOf:
            - $ref: "#/components/schemas/JobExportError"
            - type: "null"
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        download_url:
          type: [string, "null"]
          pattern: "^/v1/exports/exp_[0-9a-f]{32}/download$"
          description: Same-origin authenticated application route when ready.

    JobExportError:
      type: object
      additionalProperties: false
      required: [code, message, retryable]
      properties:
        code:
          type: string
          pattern: "^[A-Z][A-Z0-9_]{2,63}$"
        message:
          type: string
          minLength: 1
          maxLength: 500
        retryable:
          type: boolean

    ArtifactMetadata:
      type: object
      additionalProperties: false
      required:
        - id
        - job_id
        - result_id
        - kind
        - media_type
        - bytes
        - sha256
        - truncated
        - redacted
        - expires_at
        - download_url
      description: |
        Authorized metadata only. Storage provider, bucket, staging key, final
        object key, and presigned object-store URLs are intentionally absent.
      properties:
        id:
          type: string
          pattern: "^art_[0-9a-f]{32}$"
        job_id:
          type: string
          pattern: "^job_[A-Za-z0-9]+$"
        result_id:
          type: string
          pattern: "^res_[A-Za-z0-9]+$"
        kind:
          type: string
          enum: [json, html, text, markdown, raw, screenshot, network]
        media_type:
          type: string
          minLength: 1
          maxLength: 128
        bytes:
          type: integer
          minimum: 0
        sha256:
          type: string
          pattern: "^[a-f0-9]{64}$"
        truncated:
          type: boolean
        redacted:
          type: boolean
          description: True for network capture artifacts after mandatory URL/header filtering.
        expires_at:
          type: string
          format: date-time
        download_url:
          type: string
          pattern: "^/v1/artifacts/art_[0-9a-f]{32}/download$"
          description: Same-origin authenticated application route.

    JobFields:
      type: object
      required:
        - id
        - kind
        - state
        - progress
        - crawl
        - usage
        - result_retention_expires_at
        - result_content_available
        - metadata_retention_expires_at
        - metadata_available
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: "^job_[A-Za-z0-9]+$"
        kind:
          $ref: "#/components/schemas/JobKind"
        state:
          $ref: "#/components/schemas/JobState"
        progress:
          $ref: "#/components/schemas/JobProgress"
        crawl:
          oneOf:
            - $ref: "#/components/schemas/CrawlJobProgress"
            - type: "null"
          description: |
            Non-null only for Crawl jobs. These safe aggregates remain
            available after request metadata minimization and are emitted by
            the lifecycle event stream as work advances.
        usage:
          $ref: "#/components/schemas/JobUsage"
        error:
          oneOf:
            - $ref: "#/components/schemas/Error"
            - type: "null"
        result_retention_expires_at:
          type: string
          format: date-time
          description: Immutable deadline frozen from the admitted commercial plan version.
        result_content_available:
          type: boolean
          description: False at or after the retention deadline, including before physical minimization completes.
        metadata_retention_expires_at:
          type: string
          format: date-time
          description: Immutable request/diagnostic deadline; never earlier than result retention.
        metadata_available:
          type: boolean
          description: False at or after metadata expiry; only final state, aggregate progress, and usage remain.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Job:
      allOf:
        - $ref: "#/components/schemas/JobFields"
      unevaluatedProperties: false

    JobListItem:
      allOf:
        - $ref: "#/components/schemas/JobFields"
        - type: object
          required: [target_summary]
          properties:
            target_summary:
              $ref: "#/components/schemas/JobTargetSummary"
      unevaluatedProperties: false

    JobTargetSummary:
      type: object
      additionalProperties: false
      required: [state, first_url, total]
      description: |
        List-only, retention-safe target context. URL query values and
        fragments are never returned. `total` is 1 for a scrape, the submitted
        request count for a Batch, and the submitted seed count for a Crawl.
        Unsupported Job kinds use `unavailable`; expired or sensitive metadata
        uses `redacted`, with null URL and total after expiry.
      properties:
        state:
          type: string
          enum: [available, redacted, unavailable]
        first_url:
          oneOf:
            - type: string
              format: uri
              maxLength: 4096
            - type: "null"
        total:
          oneOf:
            - type: integer
              minimum: 1
            - type: "null"

    JobKind:
      type: string
      enum:
        - scrape
        - batch
        - crawl
        - browser_session
        - ai_schema_suggestion

    JobState:
      type: string
      enum:
        - queued
        - leased
        - running
        - pausing
        - paused
        - cancelling
        - cancelled
        - succeeded
        - partially_succeeded
        - failed

    JobProgress:
      type: object
      additionalProperties: false
      required: [scheduled, completed, succeeded, failed, items]
      properties:
        scheduled:
          type: integer
          minimum: 0
        completed:
          type: integer
          minimum: 0
        succeeded:
          type: integer
          minimum: 0
        failed:
          type: integer
          minimum: 0
        items:
          type: integer
          minimum: 0

    CrawlJobProgress:
      type: object
      additionalProperties: false
      required:
        - discovered_links
        - discovery_truncated_pages
        - max_depth_reached
        - frontier_pending
      properties:
        discovered_links:
          type: integer
          minimum: 0
          maximum: 50000
          description: |
            Cumulative Worker-reported discovery candidates observed on
            successfully accepted Crawl page Results before control-plane
            scope filtering and frontier deduplication.
        discovery_truncated_pages:
          type: integer
          minimum: 0
          maximum: 100
          description: Successfully accepted Crawl pages whose bounded link discovery truncated.
        max_depth_reached:
          type: integer
          minimum: 0
          maximum: 10
          description: Greatest depth of a successfully accepted Crawl page Result.
        frontier_pending:
          type: integer
          minimum: 0
          maximum: 100
          description: Scheduled Crawl pages not yet completed.

    JobUsage:
      type: object
      additionalProperties: false
      required:
        - attempts
        - download_bytes
        - browser_ms
        - proxy_bytes
        - ai_input_tokens
        - ai_output_tokens
        - credits
      properties:
        attempts:
          type: integer
          minimum: 0
        download_bytes:
          type: integer
          minimum: 0
        browser_ms:
          type: integer
          minimum: 0
        proxy_bytes:
          type: integer
          minimum: 0
        ai_input_tokens:
          type: integer
          minimum: 0
        ai_output_tokens:
          type: integer
          minimum: 0
        credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"

    JobUsageLedger:
      type: object
      additionalProperties: false
      required: [job_id, billing, items]
      properties:
        job_id:
          type: string
          pattern: "^job_[A-Za-z0-9]+$"
        billing:
          $ref: "#/components/schemas/JobUsageBilling"
        items:
          type: array
          maxItems: 6002
          items:
            $ref: "#/components/schemas/JobUsageLine"

    JobUsageSummary:
      type: object
      additionalProperties: false
      required: [job_id, billing]
      properties:
        job_id:
          type: string
          pattern: "^job_[A-Za-z0-9]+$"
        billing:
          $ref: "#/components/schemas/JobUsageBilling"

    JobUsageItemPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/JobUsageLine"
        meta:
          $ref: "#/components/schemas/PageMeta"

    JobUsageBilling:
      type: object
      additionalProperties: false
      required:
        - mode
        - state
        - pricing_version
        - reserved_credits
        - accrued_credits
        - settled_credits
        - refunded_credits
      properties:
        mode:
          type: string
          enum: [prepaid, unmetered]
        state:
          type: string
          enum: [reserved, settled, released]
        pricing_version:
          type: string
          minLength: 1
          maxLength: 128
        reserved_credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
        accrued_credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
        settled_credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
        refunded_credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"

    JobUsageLine:
      type: object
      additionalProperties: false
      required:
        - meter
        - unit
        - quantity
        - unit_credits
        - credits
        - source_event_id
        - occurred_at
      properties:
        meter:
          type: string
          enum:
            - fetch_attempt
            - page_success
            - browser_time
            - browser_second
            - stealth_second
            - download_bytes
            - download_mib
            - proxy_bytes
            - proxy_mib
            - response_wire_bytes
            - response_decoded_bytes
            - rendered_bytes
            - artifact_storage
            - webhook_delivery
        unit:
          type: string
          enum:
            [
              attempt,
              page,
              millisecond,
              second,
              byte,
              mebibyte,
              gibibyte_day,
              delivery,
            ]
        quantity:
          type: string
          pattern: "^[0-9]+\\.[0-9]{6}$"
        unit_credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
        credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
        source_event_id:
          type: string
          minLength: 1
          maxLength: 256
        occurred_at:
          type: string
          format: date-time

    BillingAdjustmentPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/BillingAdjustment"
        meta:
          $ref: "#/components/schemas/PageMeta"

    BillingAdjustment:
      type: object
      additionalProperties: false
      required:
        - id
        - provider
        - kind
        - status
        - occurred_at
        - amount_minor
        - currency
        - currency_exponent
        - reason
        - invoice_id
        - credit_effect
        - hold
      properties:
        id:
          type: string
          pattern: "^badj_[A-Za-z0-9_-]{32}$"
        provider:
          type: string
          minLength: 1
          maxLength: 128
        kind:
          type: string
          enum: [refund, dispute]
        status:
          type: string
          enum: [pending, succeeded, failed, open, won, lost]
        occurred_at:
          type: string
          format: date-time
        amount_minor:
          type: integer
          minimum: 1
        currency:
          type: string
          pattern: "^[a-z]{3}$"
        currency_exponent:
          type: integer
          minimum: 0
          maximum: 3
          description: Frozen decimal exponent for amount_minor.
        reason:
          type: [string, "null"]
          maxLength: 512
        invoice_id:
          type: [string, "null"]
          maxLength: 512
        credit_effect:
          $ref: "#/components/schemas/BillingAdjustmentCreditEffect"
        hold:
          $ref: "#/components/schemas/BillingAdjustmentHold"

    BillingAdjustmentCreditEffect:
      type: object
      additionalProperties: false
      required: [applied, target, recovered, outstanding]
      description: |
        Cumulative effect restricted to the matched paid order's own credit
        grant. Credits from any other contract or credit-pack order are never
        recovered by this adjustment.
      properties:
        applied:
          type: boolean
        target:
          type: integer
          minimum: 0
        recovered:
          type: integer
          minimum: 0
        outstanding:
          type: integer
          minimum: 0

    BillingAdjustmentHold:
      type: object
      additionalProperties: false
      required: [active, reason, outstanding_credits]
      properties:
        active:
          type: boolean
        reason:
          type: [string, "null"]
          enum:
            - refund_full
            - refund_outstanding
            - dispute_open
            - dispute_lost
            - null
          description: |
            Unrecovered credits create refund_outstanding. A full contract
            refund creates refund_full even when its credits were recovered;
            a fully recovered credit-pack refund creates no refund hold.
            Verified open or lost disputes may create their own hold.
        outstanding_credits:
          type: integer
          minimum: 0

    BillingPeriodStatementPage:
      type: object
      additionalProperties: false
      required: [data, meta]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/BillingPeriodStatement"
        meta:
          $ref: "#/components/schemas/PageMeta"

    BillingPeriodStatement:
      type: object
      additionalProperties: false
      required:
        - id
        - statement_version
        - subscription_no
        - provider
        - period_start
        - period_end
        - status
        - payment
        - jobs
        - credits
        - items
        - workspaces
      properties:
        id:
          type: string
          pattern: "^stmt_[A-Za-z0-9_-]{32}$"
        statement_version:
          type: integer
          enum: [1, 2]
          description: |
            Response schema version. New live and finalized statements use 2.
            An immutable unversioned legacy snapshot is returned as version 1
            only after its original raw JSON and digest verify.
        subscription_no:
          type: string
          minLength: 1
          maxLength: 256
        provider:
          type: string
          minLength: 1
          maxLength: 128
        period_start:
          type: string
          format: date-time
        period_end:
          type: string
          format: date-time
        status:
          type: string
          enum: [open, awaiting_settlement, closed]
        payment:
          $ref: "#/components/schemas/BillingPeriodPayment"
        jobs:
          $ref: "#/components/schemas/BillingPeriodJobs"
        credits:
          $ref: "#/components/schemas/BillingPeriodCredits"
        items:
          type: array
          items:
            $ref: "#/components/schemas/BillingPeriodUsageItem"
        workspaces:
          type: [array, "null"]
          description: |
            Version 2 attribution groups for all personal and Team execution
            billed to the subscription user. Totals reconcile exactly with the
            top-level jobs, credits, and items. Null means a version 1 finalized
            snapshot did not capture historical workspace grouping.
          items:
            $ref: "#/components/schemas/BillingPeriodWorkspaceUsage"

    BillingPeriodWorkspaceUsage:
      type: object
      additionalProperties: false
      required:
        - workspace_id
        - workspace_name
        - workspace_kind
        - jobs
        - credits
        - items
      properties:
        workspace_id:
          type: string
          pattern: "^ws_[0-9a-f]{32}$"
        workspace_name:
          type: string
          minLength: 1
          maxLength: 120
        workspace_kind:
          type: string
          enum: [personal, team]
        jobs:
          $ref: "#/components/schemas/BillingPeriodJobs"
        credits:
          $ref: "#/components/schemas/BillingPeriodCredits"
        items:
          type: array
          items:
            $ref: "#/components/schemas/BillingPeriodUsageItem"

    BillingPeriodPayment:
      type: object
      additionalProperties: false
      required:
        - amount_minor
        - currency
        - currency_exponent
        - paid_at
        - invoice_id
        - invoice_url
        - plan_name
      properties:
        amount_minor:
          type: [integer, "null"]
          minimum: 0
        currency:
          type: [string, "null"]
          pattern: "^[a-z]{3}$"
        currency_exponent:
          type: integer
          minimum: 0
          maximum: 3
          description: |
            Frozen decimal exponent for amount_minor. This remains present even
            when historical amount or currency evidence is unavailable.
        paid_at:
          type: string
          format: date-time
        invoice_id:
          type: [string, "null"]
          maxLength: 512
        invoice_url:
          type: [string, "null"]
          format: uri
          maxLength: 4096
        plan_name:
          type: [string, "null"]
          maxLength: 256

    BillingPeriodJobs:
      type: object
      additionalProperties: false
      required: [created, terminal, reservations_open]
      properties:
        created:
          type: integer
          minimum: 0
        terminal:
          type: integer
          minimum: 0
        reservations_open:
          type: integer
          minimum: 0

    BillingPeriodCredits:
      type: object
      additionalProperties: false
      required: [reserved, settled, refunded, metered]
      properties:
        reserved:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
        settled:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
        refunded:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
        metered:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"

    BillingPeriodUsageItem:
      type: object
      additionalProperties: false
      required:
        - pricing_version
        - meter
        - unit
        - quantity
        - unit_credits
        - credits
        - line_count
      properties:
        pricing_version:
          type: string
          minLength: 1
          maxLength: 128
        meter:
          type: string
          enum:
            - fetch_attempt
            - page_success
            - browser_time
            - browser_second
            - stealth_second
            - download_bytes
            - download_mib
            - proxy_bytes
            - proxy_mib
            - response_wire_bytes
            - response_decoded_bytes
            - rendered_bytes
            - artifact_storage
            - webhook_delivery
        unit:
          type: string
          enum:
            [
              attempt,
              page,
              millisecond,
              second,
              byte,
              mebibyte,
              gibibyte_day,
              delivery,
            ]
        quantity:
          type: string
          pattern: "^[0-9]+\\.[0-9]{6}$"
        unit_credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
        credits:
          type: string
          pattern: "^[0-9]+\\.[0-9]{4}$"
        line_count:
          type: integer
          minimum: 1

    FetchMode:
      type: string
      enum: [http, browser, stealth, auto]

    CreateBrowserSessionScrapeRequest:
      type: object
      additionalProperties: false
      required: [url, mode, expected_session_state_version]
      description: |
        The session identity comes exclusively from the path. Persistent
        sessions support one GET navigation attempt in a fixed browser or
        stealth engine. Proxy and target credentials are inherited from the
        session and cannot be overridden by this request.
      properties:
        expected_session_state_version:
          type: integer
          minimum: 0
          maximum: 2147483647
        url:
          type: string
          format: uri
          maxLength: 4096
        mode:
          type: string
          enum: [browser, stealth]
        request:
          allOf:
            - $ref: "#/components/schemas/RequestOptions"
            - type: object
              properties:
                method:
                  type: string
                  const: GET
                  default: GET
                auth_secret_id:
                  type: "null"
                  default: null
                header_secret_ids:
                  type: array
                  const: []
                  default: []
                cookie_jar_id:
                  type: "null"
                  default: null
        proxy:
          allOf:
            - $ref: "#/components/schemas/ProxySelection"
            - type: object
              properties:
                policy_id:
                  type: "null"
                  default: null
                sticky_key:
                  type: "null"
                  default: null
        render:
          $ref: "#/components/schemas/RenderOptions"
        block_rules:
          $ref: "#/components/schemas/BlockRules"
        extract:
          $ref: "#/components/schemas/ExtractionOptions"
        output:
          $ref: "#/components/schemas/OutputOptions"
        limits:
          allOf:
            - $ref: "#/components/schemas/ScrapeLimits"
            - type: object
              properties:
                max_attempts_total:
                  type: integer
                  const: 1
                  default: 1
        webhook_endpoint_id:
          type: [string, "null"]
          default: null
          pattern: "^wh_[0-9a-f]{32}$"
          description: Verified tenant endpoint that receives one terminal job event.

    CreateScrapeRequest:
      type: object
      additionalProperties: false
      required: [url, mode]
      allOf:
        - not:
            required: [mode, request]
            properties:
              mode:
                enum: [auto, browser, stealth]
              request:
                type: object
                required: [method]
                properties:
                  method:
                    const: POST
      properties:
        url:
          type: string
          format: uri
          maxLength: 4096
        mode:
          $ref: "#/components/schemas/FetchMode"
        request:
          $ref: "#/components/schemas/RequestOptions"
        proxy:
          $ref: "#/components/schemas/ProxySelection"
        render:
          $ref: "#/components/schemas/RenderOptions"
        block_rules:
          $ref: "#/components/schemas/BlockRules"
        extract:
          $ref: "#/components/schemas/ExtractionOptions"
        output:
          $ref: "#/components/schemas/OutputOptions"
        limits:
          $ref: "#/components/schemas/ScrapeLimits"
        webhook_endpoint_id:
          type: [string, "null"]
          default: null
          pattern: "^wh_[0-9a-f]{32}$"
          description: Verified tenant endpoint that receives one terminal job event.

    CreateBatchRequest:
      type: object
      additionalProperties: false
      required: [requests, defaults]
      properties:
        requests:
          type: array
          minItems: 2
          maxItems: 100
          description: URLs and non-null client references must be unique after normalization.
          items:
            $ref: "#/components/schemas/BatchRequest"
        defaults:
          $ref: "#/components/schemas/BatchDefaults"

    BatchRequest:
      type: object
      additionalProperties: false
      required: [url]
      properties:
        url:
          type: string
          format: uri
          maxLength: 4096
        client_ref:
          type: [string, "null"]
          default: null
          pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$"

    BatchDefaults:
      type: object
      additionalProperties: false
      required: [mode]
      properties:
        mode:
          $ref: "#/components/schemas/FetchMode"
        request:
          allOf:
            - $ref: "#/components/schemas/RequestOptions"
            - type: object
              properties:
                method:
                  const: GET
                  default: GET
        proxy:
          $ref: "#/components/schemas/ProxySelection"
        render:
          $ref: "#/components/schemas/RenderOptions"
        block_rules:
          $ref: "#/components/schemas/BlockRules"
        extract:
          $ref: "#/components/schemas/ExtractionOptions"
        output:
          $ref: "#/components/schemas/OutputOptions"
        limits:
          $ref: "#/components/schemas/ScrapeLimits"
        webhook_endpoint_id:
          type: [string, "null"]
          default: null
          pattern: "^wh_[0-9a-f]{32}$"
          description: Verified tenant endpoint that receives one aggregate terminal event.

    CreateCrawlRequest:
      type: object
      additionalProperties: false
      required: [seeds, defaults]
      properties:
        seeds:
          type: array
          minItems: 1
          maxItems: 10
          description: |
            Normalized URLs and non-null client references must be unique. All
            seeds share the first seed's scheme, effective port, and hostname scope.
          items:
            $ref: "#/components/schemas/BatchRequest"
        defaults:
          $ref: "#/components/schemas/CrawlDefaults"
        engine_rules:
          $ref: "#/components/schemas/CrawlEngineRules"
        scope:
          $ref: "#/components/schemas/CrawlScope"
        limits:
          $ref: "#/components/schemas/CrawlLimits"
        link_rules:
          $ref: "#/components/schemas/CrawlLinkRules"

    ScheduleSafeScrapeRequest:
      allOf:
        - $ref: "#/components/schemas/CreateScrapeRequest"
        - type: object
          properties:
            request:
              allOf:
                - $ref: "#/components/schemas/RequestOptions"
                - type: object
                  properties:
                    method:
                      type: string
                      const: GET
                      default: GET
            render:
              $ref: "#/components/schemas/ScheduleSafeRenderOptions"

    ScheduleSafeBatchRequest:
      allOf:
        - $ref: "#/components/schemas/CreateBatchRequest"
        - type: object
          properties:
            defaults:
              $ref: "#/components/schemas/ScheduleSafeBatchDefaults"

    ScheduleSafeBatchDefaults:
      allOf:
        - $ref: "#/components/schemas/BatchDefaults"
        - type: object
          properties:
            render:
              $ref: "#/components/schemas/ScheduleSafeRenderOptions"

    ScheduleSafeCrawlRequest:
      allOf:
        - $ref: "#/components/schemas/CreateCrawlRequest"
        - type: object
          properties:
            defaults:
              $ref: "#/components/schemas/ScheduleSafeCrawlDefaults"

    ScheduleSafeCrawlDefaults:
      allOf:
        - $ref: "#/components/schemas/CrawlDefaults"
        - type: object
          properties:
            render:
              $ref: "#/components/schemas/ScheduleSafeRenderOptions"

    ScheduleSafeRenderOptions:
      allOf:
        - $ref: "#/components/schemas/RenderOptions"
        - type: object
          properties:
            actions:
              type: array
              maxItems: 25
              description: |
                Scheduled workloads accept only repeat-safe scroll, wait,
                hover, and navigate actions.
              items:
                $ref: "#/components/schemas/ScheduleSafeAction"

    ScheduleSafeAction:
      oneOf:
        - $ref: "#/components/schemas/ScrollAction"
        - $ref: "#/components/schemas/WaitAction"
        - $ref: "#/components/schemas/HoverAction"
        - $ref: "#/components/schemas/NavigateAction"
      discriminator:
        propertyName: type

    ScheduledScrapeSpec:
      type: object
      additionalProperties: false
      required:
        - url
        - mode
        - browser_session_id
        - request
        - proxy
        - render
        - block_rules
        - extract
        - output
        - limits
        - webhook_endpoint_id
      description: |
        Fully normalized immutable scrape snapshot returned by the Schedules
        API. Admission expands every default and resolves a saved extraction
        schema to its frozen version. Scheduled workloads cannot retain a
        persistent browser session and always use GET.
      properties:
        url:
          type: string
          format: uri
          maxLength: 4096
        mode:
          $ref: "#/components/schemas/FetchMode"
        browser_session_id:
          type: "null"
          description: Always null because schedules do not support persistent browser sessions.
        request:
          $ref: "#/components/schemas/ScheduledRequestOptions"
        proxy:
          $ref: "#/components/schemas/ScheduledProxySelection"
        render:
          $ref: "#/components/schemas/ScheduledRenderOptions"
        block_rules:
          $ref: "#/components/schemas/ScheduledBlockRules"
        extract:
          $ref: "#/components/schemas/ScheduledExtractionOptions"
        output:
          $ref: "#/components/schemas/ScheduledOutputOptions"
        limits:
          $ref: "#/components/schemas/ScheduledScrapeLimits"
        webhook_endpoint_id:
          type: [string, "null"]
          pattern: "^wh_[0-9a-f]{32}$"

    ScheduledScrapeDefaults:
      type: object
      additionalProperties: false
      required:
        - mode
        - browser_session_id
        - request
        - proxy
        - render
        - block_rules
        - extract
        - output
        - limits
        - webhook_endpoint_id
      description: Fully normalized shared scrape defaults returned for a scheduled Batch or Crawl.
      properties:
        mode:
          $ref: "#/components/schemas/FetchMode"
        browser_session_id:
          type: "null"
          description: Always null because Batch and Crawl do not support persistent browser sessions.
        request:
          $ref: "#/components/schemas/ScheduledRequestOptions"
        proxy:
          $ref: "#/components/schemas/ScheduledProxySelection"
        render:
          $ref: "#/components/schemas/ScheduledRenderOptions"
        block_rules:
          $ref: "#/components/schemas/ScheduledBlockRules"
        extract:
          $ref: "#/components/schemas/ScheduledExtractionOptions"
        output:
          $ref: "#/components/schemas/ScheduledOutputOptions"
        limits:
          $ref: "#/components/schemas/ScheduledScrapeLimits"
        webhook_endpoint_id:
          type: [string, "null"]
          pattern: "^wh_[0-9a-f]{32}$"

    ScheduledBatchSpec:
      type: object
      additionalProperties: false
      required: [defaults, requests]
      description: |
        Normalized immutable ordered Batch schedule snapshot. Shared defaults
        are frozen once per schedule version, while every request returns its
        effective frozen Scrape spec so resolved extraction-schema identity and
        all normalized defaults remain reproducible at a later due slot.
      properties:
        defaults:
          $ref: "#/components/schemas/ScheduledScrapeDefaults"
        requests:
          type: array
          minItems: 2
          maxItems: 100
          items:
            type: object
            additionalProperties: false
            required: [client_ref, spec]
            properties:
              client_ref:
                type: [string, "null"]
                maxLength: 64
                pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$"
              spec:
                $ref: "#/components/schemas/ScheduledScrapeSpec"

    ScheduledRequestOptions:
      allOf:
        - $ref: "#/components/schemas/RequestOptions"
        - type: object
          required:
            - method
            - query
            - headers
            - body
            - auth_secret_id
            - header_secret_ids
            - cookie_jar_id
          properties:
            method:
              type: string
              const: GET

    ScheduledProxySelection:
      allOf:
        - $ref: "#/components/schemas/ProxySelection"
        - type: object
          required: [policy_id, sticky_key]

    ScheduledRenderOptions:
      allOf:
        - $ref: "#/components/schemas/RenderOptions"
        - type: object
          required:
            - wait_until
            - wait_for
            - wait_ms
            - block_resources
            - block_ads
            - challenge_handling
            - actions
            - network_capture
          properties:
            wait_for:
              oneOf:
                - $ref: "#/components/schemas/ScheduledWaitFor"
                - type: "null"
            actions:
              type: array
              maxItems: 25
              items:
                $ref: "#/components/schemas/ScheduledAction"
            network_capture:
              $ref: "#/components/schemas/ScheduledNetworkCaptureOptions"

    ScheduledWaitFor:
      allOf:
        - $ref: "#/components/schemas/WaitFor"
        - type: object
          required: [selector, state]
          properties:
            selector:
              type: string
              minLength: 1
              maxLength: 2048
            state:
              type: string
              enum: [attached, detached, visible, hidden]

    ScheduledNetworkCaptureOptions:
      allOf:
        - $ref: "#/components/schemas/NetworkCaptureOptions"
        - type: object
          required:
            - enabled
            - url_patterns
            - include_body
            - max_entries
            - max_body_bytes
            - max_total_body_bytes

    ScheduledAction:
      oneOf:
        - $ref: "#/components/schemas/ScheduledScrollAction"
        - $ref: "#/components/schemas/ScheduledWaitAction"
        - $ref: "#/components/schemas/ScheduledHoverAction"
        - $ref: "#/components/schemas/ScheduledNavigateAction"
      discriminator:
        propertyName: type

    ScheduledActionCondition:
      allOf:
        - $ref: "#/components/schemas/ActionCondition"
        - type: object
          required: [selector, state]

    ScheduledScrollAction:
      allOf:
        - $ref: "#/components/schemas/ScrollAction"
        - type: object
          required: [type, x, y]
          properties:
            x:
              type: integer
              minimum: -100000
              maximum: 100000
            y:
              type: integer
              minimum: -100000
              maximum: 100000
            when:
              $ref: "#/components/schemas/ScheduledActionCondition"

    ScheduledWaitAction:
      allOf:
        - $ref: "#/components/schemas/WaitAction"
        - type: object
          properties:
            when:
              $ref: "#/components/schemas/ScheduledActionCondition"

    ScheduledHoverAction:
      allOf:
        - $ref: "#/components/schemas/HoverAction"
        - type: object
          properties:
            when:
              $ref: "#/components/schemas/ScheduledActionCondition"

    ScheduledNavigateAction:
      allOf:
        - $ref: "#/components/schemas/NavigateAction"
        - type: object
          required: [type, url, wait_until]
          properties:
            url:
              type: string
              format: uri
              minLength: 1
              maxLength: 4096
            wait_until:
              type: string
              enum: [commit, domcontentloaded, load, networkidle]
            when:
              $ref: "#/components/schemas/ScheduledActionCondition"

    ScheduledBlockRules:
      allOf:
        - $ref: "#/components/schemas/BlockRules"
        - type: object
          required:
            - status_codes
            - title_contains
            - body_contains
            - challenge_markers

    ScheduledExtractionOptions:
      type: object
      additionalProperties: false
      required: [schema_id, schema_version, fields]
      description: |
        Frozen extraction definition. `schema_version` is an output-only
        resolved version and is present on every Schedule response; it is null
        together with `schema_id` when the snapshot does not use a saved schema.
      oneOf:
        - properties:
            schema_id:
              type: "null"
            schema_version:
              type: "null"
        - properties:
            schema_id:
              type: string
              pattern: "^xs_[0-9a-f]{32}$"
            schema_version:
              type: integer
              minimum: 1
      properties:
        schema_id:
          type: [string, "null"]
          pattern: "^xs_[0-9a-f]{32}$"
        schema_version:
          type: [integer, "null"]
          minimum: 1
          readOnly: true
        fields:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/ScheduledExtractionField"

    ScheduledExtractionField:
      allOf:
        - $ref: "#/components/schemas/ExtractionField"
        - type: object
          required: [name, selector, many, required, transforms]
          properties:
            selector:
              $ref: "#/components/schemas/ScheduledSelector"

    ScheduledSelector:
      allOf:
        - $ref: "#/components/schemas/Selector"
        - type: object
          required: [type, value, attribute, adaptive]

    ScheduledOutputOptions:
      allOf:
        - $ref: "#/components/schemas/OutputOptions"
        - type: object
          required: [formats, screenshot]
          properties:
            screenshot:
              $ref: "#/components/schemas/ScheduledScreenshotOptions"

    ScheduledScreenshotOptions:
      allOf:
        - $ref: "#/components/schemas/ScreenshotOptions"
        - type: object
          required: [enabled, format, full_page]

    ScheduledScrapeLimits:
      allOf:
        - $ref: "#/components/schemas/ScrapeLimits"
        - type: object
          required: [timeout_ms, max_response_bytes, max_attempts_total]

    ScheduledCrawlScope:
      allOf:
        - $ref: "#/components/schemas/CrawlScope"
        - type: object
          required:
            - include_subdomains
            - allow_path_prefixes
            - deny_path_prefixes

    ScheduledCrawlLimits:
      allOf:
        - $ref: "#/components/schemas/CrawlLimits"
        - type: object
          required:
            - max_pages
            - max_depth
            - max_duration_ms
            - max_download_bytes
            - max_links_per_page
            - request_delay_ms
            - respect_robots

    ScheduledCrawlSpec:
      type: object
      additionalProperties: false
      required:
        - defaults
        - seeds
        - crawl_rules_version
        - engine_rules
        - link_rules_version
        - link_rules
        - scope
        - limits
      description: |
        Normalized immutable Crawl schedule snapshot. Each seed contains its
        effective frozen Scrape spec; engine rules remain ordered and the
        version marker is always explicit in Schedule responses.
      properties:
        defaults:
          $ref: "#/components/schemas/ScheduledScrapeDefaults"
        seeds:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: object
            additionalProperties: false
            required: [client_ref, spec]
            properties:
              client_ref:
                type: [string, "null"]
                maxLength: 64
                pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$"
              spec:
                $ref: "#/components/schemas/ScheduledScrapeSpec"
        crawl_rules_version:
          type: integer
          enum: [0, 1]
        engine_rules:
          $ref: "#/components/schemas/CrawlEngineRules"
        link_rules_version:
          type: integer
          enum: [0, 1]
          description: |
            Zero preserves compatible legacy a[href] discovery. One
            identifies a normalized non-empty LinkExtractor rule snapshot.
        link_rules:
          $ref: "#/components/schemas/ScheduledCrawlLinkRules"
        scope:
          $ref: "#/components/schemas/ScheduledCrawlScope"
        limits:
          $ref: "#/components/schemas/ScheduledCrawlLimits"

    CrawlDefaults:
      type: object
      additionalProperties: false
      required: [mode]
      properties:
        mode:
          type: string
          enum: [auto, http, browser, stealth]
          description: |
            Fallback execution strategy for seeds and discovered pages that do
            not match an engine rule. Auto uses the least capable pool required
            by render settings and does not opt the crawl into proxy routing.
        browser_session_id:
          type: "null"
          default: null
          description: Persistent browser sessions are not supported by Crawl.
        request:
          allOf:
            - $ref: "#/components/schemas/RequestOptions"
            - type: object
              properties:
                method:
                  const: GET
                  default: GET
        proxy:
          $ref: "#/components/schemas/ProxySelection"
        render:
          $ref: "#/components/schemas/RenderOptions"
        block_rules:
          $ref: "#/components/schemas/BlockRules"
        extract:
          $ref: "#/components/schemas/ExtractionOptions"
        output:
          $ref: "#/components/schemas/OutputOptions"
        limits:
          $ref: "#/components/schemas/ScrapeLimits"
        webhook_endpoint_id:
          type: [string, "null"]
          default: null
          pattern: "^wh_[0-9a-f]{32}$"

    CrawlEngineRules:
      type: array
      maxItems: 20
      uniqueItems: true
      default: []
      description: |
        Ordered first-match overrides evaluated only after a normalized URL
        passes Crawl destination and scope policy. Path prefixes are normalized
        to WHATWG pathname form with canonical percent escapes and must still
        fit the 512-character bound after normalization. Duplicate prefixes and
        rules fully shadowed by an earlier broader prefix are rejected. Rules
        choose a fixed engine only; they never select a proxy or bypass SSRF,
        scope, robots.txt, or redirect checks. Render, action, extraction,
        output, request, and limit settings remain shared defaults.
      items:
        $ref: "#/components/schemas/CrawlEngineRule"

    CrawlEngineRule:
      type: object
      additionalProperties: false
      required: [match, mode]
      properties:
        match:
          type: object
          additionalProperties: false
          required: [path_prefix]
          properties:
            path_prefix:
              type: string
              minLength: 1
              maxLength: 512
              pattern: "^/[^?#\\\\]*$"
        mode:
          type: string
          enum: [http, browser, stealth]

    CrawlLinkRules:
      type: array
      maxItems: 20
      default: []
      description: |
        Ordered, declarative, non-callable LinkExtractor rules. An empty array
        preserves compatible legacy discovery of a[href] links with no
        extension filter. A non-empty array is evaluated in rule order; links
        within each rule retain document order and the first canonical URL
        occurrence wins across rules. The normalized JSON representation of
        the complete array may not exceed 64 KiB. It also remains inside the
        Crawl 1 MiB expanded-snapshot and 900 KiB queue-envelope boundaries.
        Rules never widen Crawl scope, bypass robots.txt/SSRF/redirect checks,
        choose a proxy, or accept callbacks/processors.
      items:
        $ref: "#/components/schemas/CrawlLinkRule"

    CrawlLinkRule:
      type: object
      additionalProperties: false
      description: |
        URL globs are matched against the complete canonical absolute URL.
        Only `*` (zero or more Unicode code points) and `?` (one Unicode code
        point) are special; all other characters are literal. Consecutive `*`
        characters normalize to one. Deny takes precedence within one rule;
        any rule that passes all of its filters may admit the URL.
        CSS/XPath scopes apply only to document extraction. Sitemap candidates
        apply URL, domain, and extension filters only. CSS and XPath accept
        only the bounded safe subsets described by their component schemas;
        combinators, functions, axes, unions, pseudo-selectors, and escapes
        are rejected at admission.
      properties:
        allow:
          $ref: "#/components/schemas/CrawlLinkGlobs"
        deny:
          $ref: "#/components/schemas/CrawlLinkGlobs"
        allow_domains:
          $ref: "#/components/schemas/CrawlLinkDomains"
        deny_domains:
          $ref: "#/components/schemas/CrawlLinkDomains"
        restrict_css:
          $ref: "#/components/schemas/CrawlLinkCssSelectors"
        restrict_xpath:
          $ref: "#/components/schemas/CrawlLinkXpathSelectors"
        tags:
          type: array
          minItems: 1
          maxItems: 8
          uniqueItems: true
          default: [a, area]
          items:
            type: string
            minLength: 1
            maxLength: 64
            pattern: "^[a-z][a-z0-9-]{0,63}$"
        attrs:
          type: array
          minItems: 1
          maxItems: 8
          uniqueItems: true
          default: [href]
          items:
            type: string
            minLength: 1
            maxLength: 64
            pattern: "^[a-z][a-z0-9-]{0,63}$"
        deny_extensions:
          type: array
          maxItems: 128
          uniqueItems: true
          default: []
          description: |
            Explicit lowercase file extensions without a leading dot. The
            default is deliberately empty to preserve existing Crawl behavior.
            Consecutive dots are rejected.
          items:
            type: string
            minLength: 1
            maxLength: 32
            pattern: "^[a-z0-9](?:[a-z0-9.+-]{0,30}[a-z0-9])?$"
            not:
              pattern: "[.][.]"

    CrawlLinkGlobs:
      type: array
      maxItems: 8
      uniqueItems: true
      default: []
      description: |
        Portable full-string patterns matched against canonical absolute URLs.
        Values normalize consecutive `*` characters before duplicate detection.
      items:
        type: string
        minLength: 1
        maxLength: 256

    CrawlLinkDomains:
      type: array
      maxItems: 8
      uniqueItems: true
      default: []
      description: |
        ASCII/IDNA hostnames without scheme, credentials, port, path, query, or
        fragment. Slash, backslash, whitespace, multiple/empty labels, and
        leading dots are rejected. Values normalize to lowercase and one
        trailing dot is removed. Exact hosts and their true subdomains match.
      items:
        type: string
        minLength: 1
        maxLength: 253

    CrawlLinkCssSelectors:
      type: array
      maxItems: 8
      uniqueItems: true
      default: []
      description: |
        Safe simple-compound CSS scopes only: an optional lowercase type name
        or `*`, followed by at most eight `#id`, `.class`, `[attribute]`, or
        `[attribute="literal"]` qualifiers, with at most one `#id`. Identifier
        tokens are at most 64 characters and quoted literals at most 128
        characters. Combinators, selector lists, pseudo-selectors, escapes, and
        other attribute operators are rejected.
      items:
        type: string
        minLength: 1
        maxLength: 512

    CrawlLinkXpathSelectors:
      type: array
      maxItems: 8
      uniqueItems: true
      default: []
      description: |
        Safe descendant-only XPath scopes: `//tag` or `//*`, optionally
        followed by exactly one `[@attribute]`,
        `[@attribute="literal"]`, or `[@attribute='literal']` predicate.
        Names are lowercase and at most 64 characters; literals are at most
        128 characters. Other axes, nested paths, functions, unions, and
        escapes are rejected.
      items:
        type: string
        minLength: 1
        maxLength: 512

    ScheduledCrawlLinkRules:
      type: array
      maxItems: 20
      description: |
        Normalized immutable LinkExtractor rules returned in a Crawl schedule
        snapshot. Every rule field is explicit.
      items:
        $ref: "#/components/schemas/ScheduledCrawlLinkRule"

    ScheduledCrawlLinkRule:
      allOf:
        - $ref: "#/components/schemas/CrawlLinkRule"
        - type: object
          required:
            - allow
            - deny
            - allow_domains
            - deny_domains
            - restrict_css
            - restrict_xpath
            - tags
            - attrs
            - deny_extensions

    CrawlScope:
      type: object
      additionalProperties: false
      properties:
        include_subdomains:
          type: boolean
          default: false
        allow_path_prefixes:
          $ref: "#/components/schemas/CrawlPathPrefixes"
        deny_path_prefixes:
          $ref: "#/components/schemas/CrawlPathPrefixes"

    CrawlPathPrefixes:
      type: array
      maxItems: 20
      uniqueItems: true
      default: []
      items:
        type: string
        minLength: 1
        maxLength: 512
        pattern: "^/[^?#\\\\]*$"

    CrawlLimits:
      type: object
      additionalProperties: false
      properties:
        max_pages:
          type: integer
          minimum: 1
          maximum: 100
          default: 100
          description: |
            Current admitted public page ceiling. The 10,000-page scale target
            remains a promotion gate and is not accepted by this schema.
          x-toptrends-promotion-target: 10000
          x-toptrends-promotion-status: gated
        max_depth:
          type: integer
          minimum: 0
          maximum: 10
          default: 3
        max_duration_ms:
          type: integer
          minimum: 10000
          maximum: 900000
          default: 900000
        max_download_bytes:
          type: integer
          minimum: 1024
          maximum: 104857600
          default: 104857600
        max_links_per_page:
          type: integer
          minimum: 1
          maximum: 500
          default: 500
        request_delay_ms:
          type: integer
          minimum: 1000
          maximum: 60000
          default: 1000
        respect_robots:
          type: boolean
          const: true
          default: true

    RequestOptions:
      type: object
      additionalProperties: false
      properties:
        method:
          type: string
          enum: [GET, POST]
          default: GET
        query:
          type: object
          maxProperties: 100
          additionalProperties:
            type: string
            maxLength: 4096
        headers:
          type: object
          maxProperties: 50
          description: |
            Raw non-secret target headers. Values are stored in immutable job
            metadata and dispatch envelopes; use header_secret_ids,
            auth_secret_id, or cookie_jar_id for credentials. Raw and
            secret-backed names together may total at most 50 and must be
            semantically unique case-insensitively. Content-Length,
            Proxy-Connection, __proto__, X-HTTP-Method,
            X-HTTP-Method-Override, X-Method-Override, and other
            platform-managed names are rejected.
            The UTF-8 byte lengths of every materialized name and value,
            including secret-backed headers, may total at most 32768 bytes.
            Headers are sent only to the normalized initial exact origin (same
            scheme, hostname, and effective port). Their values are never sent
            on a cross-origin HTTP redirect or browser/stealth navigation,
            XHR, fetch, or subresource request.
          additionalProperties:
            type: string
            maxLength: 8192
        body:
          oneOf:
            - type: object
              maxProperties: 500
              additionalProperties: true
            - type: string
              maxLength: 1048576
            - type: "null"
        auth_secret_id:
          type: [string, "null"]
          pattern: "^sec_[0-9a-f]{32}$"
          default: null
          description: |
            Active authorization-kind secret owned by the same workspace.
            Browser/stealth execution injects it only through the trusted route
            for the normalized initial exact origin.
        header_secret_ids:
          type: array
          maxItems: 25
          uniqueItems: true
          default: []
          description: |
            Active same-workspace header_value secret references. Each secret
            has an immutable header name and exact public target origin. Create
            and estimate validate every binding; the lease resolves values
            without placing values in job metadata, Outbox, or dispatch
            envelopes. Opaque IDs remain in immutable job metadata but are
            omitted from the JetStream queue.
          items:
            type: string
            pattern: "^sec_[0-9a-f]{32}$"
        cookie_jar_id:
          type: [string, "null"]
          pattern: "^cj_[0-9a-f]{32}$"
          default: null
          description: |
            Active cookie jar whose exact origin matches the normalized target.
            HTTP redirects drop its cookies cross-origin. Browser/stealth
            execution aborts every cross-origin request before network while
            the jar is installed.

    ProxySelection:
      type: object
      additionalProperties: false
      properties:
        policy_id:
          type: [string, "null"]
          pattern: "^pp_[A-Za-z0-9]+$"
        sticky_key:
          type: [string, "null"]
          minLength: 1
          maxLength: 128

    RenderOptions:
      type: object
      additionalProperties: false
      properties:
        wait_until:
          type: string
          enum: [load, networkidle]
          default: load
        wait_for:
          oneOf:
            - $ref: "#/components/schemas/WaitFor"
            - type: "null"
        wait_ms:
          type: integer
          minimum: 0
          maximum: 30000
          default: 0
        block_resources:
          type: array
          uniqueItems: true
          maxItems: 6
          items:
            type: string
            enum: [image, font, media, stylesheet, websocket, eventsource]
        block_ads:
          type: boolean
          default: false
        challenge_handling:
          type: string
          enum: [off, auto]
          default: off
        actions:
          type: array
          maxItems: 25
          description: |
            Declarative browser actions. click, press, and double_click are
            single-shot: after lease activation, a failed result or lost lease
            terminates as SIDE_EFFECT_UNCONFIRMED and the surrounding action
            sequence is not automatically retried.
          items:
            $ref: "#/components/schemas/Action"
        network_capture:
          $ref: "#/components/schemas/NetworkCaptureOptions"

    NetworkCaptureOptions:
      type: object
      additionalProperties: false
      description: |
        Browser/stealth-only XHR and fetch response capture. Disabled by
        default. Matching uses the original URL, but persisted URLs omit query
        strings, fragments, and credentials. Request headers are never
        captured; response headers use the normal safe allowlist. Bodies are
        base64 encoded only after the actual response size fits both body
        budgets. Every omission caused by a limit is marked truncated.
      properties:
        enabled:
          type: boolean
          default: false
        url_patterns:
          type: array
          uniqueItems: true
          maxItems: 10
          items:
            type: string
            minLength: 1
            maxLength: 512
          description: Case-sensitive glob patterns; an empty list matches all XHR/fetch URLs.
        include_body:
          type: boolean
          default: true
        max_entries:
          type: integer
          minimum: 1
          maximum: 50
          default: 20
        max_body_bytes:
          type: integer
          minimum: 1
          maximum: 131072
          default: 65536
        max_total_body_bytes:
          type: integer
          minimum: 1
          maximum: 524288
          default: 524288

    BlockRules:
      type: object
      additionalProperties: false
      description: |
        Optional deterministic response-block classification. Matching is
        case-insensitive, uses OR semantics, and never accepts regular
        expressions or callbacks. Title and body matching inspect only the
        first 524288 response bytes; extracted title text is additionally
        capped at 4096 characters. Marker arrays have an aggregate UTF-8 limit
        of 8192 bytes. A match produces a retryable HTTP_BLOCKED or
        CHALLENGE_FAILED result without reflecting the configured marker.
      properties:
        status_codes:
          type: array
          uniqueItems: true
          maxItems: 20
          default: []
          items:
            type: integer
            minimum: 100
            maximum: 599
        title_contains:
          type: array
          uniqueItems: true
          maxItems: 10
          default: []
          items:
            type: string
            minLength: 3
            maxLength: 256
        body_contains:
          type: array
          uniqueItems: true
          maxItems: 10
          default: []
          items:
            type: string
            minLength: 3
            maxLength: 256
        challenge_markers:
          type: array
          uniqueItems: true
          maxItems: 10
          default: []
          items:
            type: string
            minLength: 3
            maxLength: 256

    WaitFor:
      type: object
      additionalProperties: false
      required: [selector]
      properties:
        selector:
          type: string
          minLength: 1
          maxLength: 2048
        state:
          type: string
          enum: [attached, detached, visible, hidden]
          default: visible
        timeout_ms:
          type: integer
          minimum: 1
          maximum: 30000

    Action:
      oneOf:
        - $ref: "#/components/schemas/ClickAction"
        - $ref: "#/components/schemas/FillAction"
        - $ref: "#/components/schemas/PressAction"
        - $ref: "#/components/schemas/ScrollAction"
        - $ref: "#/components/schemas/WaitAction"
        - $ref: "#/components/schemas/HoverAction"
        - $ref: "#/components/schemas/DoubleClickAction"
        - $ref: "#/components/schemas/SelectAction"
        - $ref: "#/components/schemas/NavigateAction"
      discriminator:
        propertyName: type

    ActionCondition:
      type: object
      additionalProperties: false
      required: [selector]
      properties:
        selector:
          type: string
          minLength: 1
          maxLength: 2048
        state:
          type: string
          enum: [attached, detached, visible, hidden]
          default: visible

    ClickAction:
      type: object
      additionalProperties: false
      required: [type, selector]
      properties:
        type:
          const: click
        selector:
          type: string
          minLength: 1
          maxLength: 2048
        timeout_ms:
          type: integer
          minimum: 1
          maximum: 30000
        when:
          $ref: "#/components/schemas/ActionCondition"

    FillAction:
      type: object
      additionalProperties: false
      required: [type, selector]
      oneOf:
        - required: [value]
          properties:
            value:
              type: string
              maxLength: 8192
        - required: [value_secret_id]
          properties:
            value_secret_id:
              type: string
              pattern: "^sec_[0-9a-f]{32}$"
      properties:
        type:
          const: fill
        selector:
          type: string
          minLength: 1
          maxLength: 2048
        value:
          type: string
          maxLength: 8192
        value_secret_id:
          type: string
          pattern: "^sec_[0-9a-f]{32}$"
          description: Active action_value-kind secret owned by the same workspace.
        timeout_ms:
          type: integer
          minimum: 1
          maximum: 30000
        when:
          $ref: "#/components/schemas/ActionCondition"
        max_attempts:
          type: integer
          minimum: 1
          maximum: 3
        retry_delay_ms:
          type: integer
          minimum: 0
          maximum: 2000

    PressAction:
      type: object
      additionalProperties: false
      required: [type, selector, key]
      properties:
        type:
          const: press
        selector:
          type: string
          minLength: 1
          maxLength: 2048
        key:
          type: string
          enum: [Enter, Tab, Escape, ArrowUp, ArrowDown, ArrowLeft, ArrowRight]
        timeout_ms:
          type: integer
          minimum: 1
          maximum: 30000
        when:
          $ref: "#/components/schemas/ActionCondition"

    ScrollAction:
      type: object
      additionalProperties: false
      required: [type]
      properties:
        type:
          const: scroll
        x:
          type: integer
          minimum: -100000
          maximum: 100000
          default: 0
        y:
          type: integer
          minimum: -100000
          maximum: 100000
          default: 0
        when:
          $ref: "#/components/schemas/ActionCondition"

    WaitAction:
      type: object
      additionalProperties: false
      required: [type, duration_ms]
      properties:
        type:
          const: wait
        duration_ms:
          type: integer
          minimum: 1
          maximum: 30000
        when:
          $ref: "#/components/schemas/ActionCondition"

    HoverAction:
      type: object
      additionalProperties: false
      required: [type, selector]
      properties:
        type:
          const: hover
        selector:
          type: string
          minLength: 1
          maxLength: 2048
        timeout_ms:
          type: integer
          minimum: 1
          maximum: 30000
        when:
          $ref: "#/components/schemas/ActionCondition"
        max_attempts:
          type: integer
          minimum: 1
          maximum: 3
        retry_delay_ms:
          type: integer
          minimum: 0
          maximum: 2000

    DoubleClickAction:
      type: object
      additionalProperties: false
      required: [type, selector]
      properties:
        type:
          const: double_click
        selector:
          type: string
          minLength: 1
          maxLength: 2048
        timeout_ms:
          type: integer
          minimum: 1
          maximum: 30000
        when:
          $ref: "#/components/schemas/ActionCondition"

    SelectAction:
      type: object
      additionalProperties: false
      required: [type, selector]
      oneOf:
        - required: [value]
          properties:
            value: {}
        - required: [label]
          properties:
            label: {}
        - required: [index]
          properties:
            index: {}
      properties:
        type:
          const: select
        selector:
          type: string
          minLength: 1
          maxLength: 2048
        value:
          type: string
          maxLength: 2048
        label:
          type: string
          maxLength: 2048
        index:
          type: integer
          minimum: 0
          maximum: 100000
        timeout_ms:
          type: integer
          minimum: 1
          maximum: 30000
        when:
          $ref: "#/components/schemas/ActionCondition"
        max_attempts:
          type: integer
          minimum: 1
          maximum: 3
        retry_delay_ms:
          type: integer
          minimum: 0
          maximum: 2000

    NavigateAction:
      type: object
      additionalProperties: false
      required: [type, url]
      properties:
        type:
          const: navigate
        url:
          type: string
          format: uri
          minLength: 1
          maxLength: 4096
          description: Public HTTP(S) destination checked by the normal egress policy.
        wait_until:
          type: string
          enum: [commit, domcontentloaded, load, networkidle]
          default: load
        timeout_ms:
          type: integer
          minimum: 1
          maximum: 30000
        when:
          $ref: "#/components/schemas/ActionCondition"
        max_attempts:
          type: integer
          minimum: 1
          maximum: 3
        retry_delay_ms:
          type: integer
          minimum: 0
          maximum: 2000

    ExtractionOptions:
      type: object
      additionalProperties: false
      description: |
        Provide either a published tenant `schema_id` or inline `fields`, never
        both. Admission freezes the published schema version and normalized
        fields into the job so later publication changes cannot alter replays.
        Inline fields cannot enable adaptive matching.
      properties:
        schema_id:
          type: [string, "null"]
          default: null
          pattern: "^xs_[0-9a-f]{32}$"
          description: Active tenant schema with a published immutable version.
        fields:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/InlineExtractionField"

    ExtractionField:
      type: object
      additionalProperties: false
      required: [name, selector]
      allOf:
        - if:
            properties:
              selector:
                type: object
                required: [adaptive]
                properties:
                  adaptive:
                    const: true
          then:
            properties:
              selector:
                allOf:
                  - $ref: "#/components/schemas/Selector"
                  - properties:
                      type:
                        enum: [css, xpath]
              many:
                const: false
      properties:
        name:
          type: string
          pattern: "^[A-Za-z_][A-Za-z0-9_]{0,63}$"
        selector:
          $ref: "#/components/schemas/Selector"
        many:
          type: boolean
          default: false
        required:
          type: boolean
          default: false
        transforms:
          type: array
          maxItems: 10
          items:
            type: string
            enum:
              [
                trim,
                normalize_whitespace,
                lowercase,
                uppercase,
                to_integer,
                to_number,
                to_boolean,
              ]

    Selector:
      type: object
      additionalProperties: false
      required: [type, value]
      properties:
        type:
          type: string
          enum: [css, xpath, json_path, regex]
        value:
          type: string
          minLength: 1
          maxLength: 4096
        attribute:
          type: [string, "null"]
          maxLength: 128
        adaptive:
          type: boolean
          default: false
          description: |
            Allowed only for single-value CSS/XPath fields inside a saved
            tenant schema. Inline requests with true are rejected.

    InlineExtractionField:
      allOf:
        - $ref: "#/components/schemas/ExtractionField"
        - properties:
            selector:
              allOf:
                - $ref: "#/components/schemas/Selector"
                - properties:
                    adaptive:
                      const: false

    OutputOptions:
      type: object
      additionalProperties: false
      properties:
        formats:
          type: array
          uniqueItems: true
          minItems: 1
          maxItems: 5
          items:
            type: string
            enum: [json, html, text, markdown, raw]
        screenshot:
          $ref: "#/components/schemas/ScreenshotOptions"

    ScreenshotOptions:
      type: object
      additionalProperties: false
      properties:
        enabled:
          type: boolean
          default: false
        format:
          type: string
          enum: [png, jpeg]
          default: png
        full_page:
          type: boolean
          default: true
        quality:
          type: integer
          minimum: 1
          maximum: 100

    ScrapeLimits:
      type: object
      additionalProperties: false
      properties:
        timeout_ms:
          type: integer
          minimum: 1000
          maximum: 180000
          default: 60000
          description: |
            Per-page execution timeout. A fixed `http` mode or engine rule has
            the narrower 120000 ms server ceiling.
        max_response_bytes:
          type: integer
          minimum: 1024
          maximum: 52428800
        max_attempts_total:
          type: integer
          minimum: 1
          maximum: 5
          default: 3

    Result:
      type: object
      additionalProperties: false
      required:
        - id
        - job_id
        - client_ref
        - crawl_depth
        - request_url
        - final_url
        - status
        - engine
        - started_at
        - finished_at
        - elapsed_ms
        - http_status
        - response_headers
        - extraction_evidence
        - data
        - error
        - artifacts
        - created_at
      properties:
        id:
          type: string
          pattern: "^res_[0-9a-f]{32}$"
        job_id:
          type: string
          pattern: "^job_[0-9a-f]{32}$"
        client_ref:
          type: [string, "null"]
          maxLength: 128
        crawl_depth:
          type: [integer, "null"]
          minimum: 0
          maximum: 10
        request_url:
          type: string
          format: uri
          maxLength: 4096
        final_url:
          type: [string, "null"]
          format: uri
          maxLength: 8192
        status:
          type: string
          enum: [succeeded, failed]
        engine:
          type: string
          enum: [http, browser, stealth]
        started_at:
          type: string
          format: date-time
        finished_at:
          type: string
          format: date-time
        elapsed_ms:
          type: integer
          minimum: 0
        http_status:
          type: [integer, "null"]
          minimum: 100
          maximum: 599
        response_headers:
          type: object
          additionalProperties:
            type: string
            maxLength: 8192
          maxProperties: 7
          propertyNames:
            enum:
              - cache-control
              - content-language
              - content-length
              - content-type
              - etag
              - last-modified
          description: |
            A bounded allowlist only. Authorization, Cookie, Set-Cookie,
            Proxy-Authorization, hop-by-hop, and internal diagnostic headers
            are never returned.
        extraction_evidence:
          type: array
          maxItems: 100
          description: |
            Privacy-minimal adaptive match evidence. Structural fingerprints,
            target text, and attribute values are never returned.
          items:
            $ref: "#/components/schemas/AdaptiveExtractionEvidence"
        data:
          type: [object, array, string, number, boolean, "null"]
        error:
          oneOf:
            - $ref: "#/components/schemas/ResultError"
            - type: "null"
        artifacts:
          type: array
          items:
            $ref: "#/components/schemas/Artifact"
        created_at:
          type: string
          format: date-time

    AdaptiveExtractionEvidence:
      type: object
      additionalProperties: false
      required:
        - field_name
        - match
        - confidence
        - schema_id
        - schema_version
        - observed_evidence_hash
        - baseline_evidence_hash
      properties:
        field_name:
          type: string
          pattern: "^[A-Za-z_][A-Za-z0-9_]{0,63}$"
        match:
          type: string
          enum: [exact, relocated]
        confidence:
          type: integer
          minimum: 7000
          maximum: 10000
          description: Structural confidence in basis points.
        schema_id:
          type: string
          pattern: "^xs_[0-9a-f]{32}$"
        schema_version:
          type: integer
          minimum: 1
          maximum: 1000000
        observed_evidence_hash:
          type: string
          pattern: "^[0-9a-f]{64}$"
        baseline_evidence_hash:
          type: string
          pattern: "^[0-9a-f]{64}$"

    Artifact:
      type: object
      additionalProperties: false
      required:
        - id
        - job_id
        - result_id
        - kind
        - media_type
        - bytes
        - sha256
        - truncated
        - redacted
        - expires_at
        - download_url
      description: |
        An authorized public descriptor. Storage provider, bucket, object key,
        inline bytes, and presigned object-store URLs are intentionally absent.
      properties:
        id:
          type: string
          pattern: "^art_[0-9a-f]{32}$"
        job_id:
          type: string
          pattern: "^job_[0-9a-f]{32}$"
        result_id:
          type: string
          pattern: "^res_[0-9a-f]{32}$"
        kind:
          type: string
          enum: [json, html, text, markdown, raw, screenshot, network]
        media_type:
          type: string
          minLength: 1
          maxLength: 128
        bytes:
          type: integer
          minimum: 0
        sha256:
          type: string
          pattern: "^[a-f0-9]{64}$"
        truncated:
          type: boolean
        redacted:
          type: boolean
          description: True for network capture artifacts after mandatory URL/header filtering.
        expires_at:
          type: string
          format: date-time
        download_url:
          type: string
          pattern: "^/v1/artifacts/art_[0-9a-f]{32}/download$"
          description: Same-origin authenticated application route.

    ResultError:
      type: object
      additionalProperties: false
      required: [code, message, retryable]
      properties:
        code:
          type: string
          description: |
            SIDE_EFFECT_UNCONFIRMED means an active POST or a browser action
            sequence containing click, press, or double_click may have mutated
            the target and will not be retried automatically.
          pattern: "^[A-Z][A-Z0-9_]{0,63}$"
        message:
          type: string
          minLength: 1
          maxLength: 1000
        retryable:
          type: boolean

    PageMeta:
      type: object
      additionalProperties: false
      required: [next_cursor]
      properties:
        next_cursor:
          type: [string, "null"]

    PublicApiProblem:
      type: object
      additionalProperties: false
      required:
        - type
        - title
        - status
        - detail
        - instance
        - code
        - request_id
      properties:
        type:
          type: string
          pattern: "^urn:toptrends:error:[a-z_]+$"
        title:
          type: string
          maxLength: 128
        status:
          type: integer
          minimum: 400
          maximum: 599
        detail:
          type: string
          maxLength: 1000
        instance:
          type: string
          maxLength: 4096
        code:
          type: string
          enum:
            - INVALID_REQUEST
            - UNAUTHENTICATED
            - FORBIDDEN
            - NOT_FOUND
            - CONFLICT
            - RATE_LIMITED
            - INSUFFICIENT_CREDITS
            - URL_BLOCKED
            - POLICY_DENIED
            - PROXY_UNAVAILABLE
            - LIMIT_EXCEEDED
            - SIDE_EFFECT_UNCONFIRMED
            - CANCELLED
            - SERVICE_UNAVAILABLE
            - INTERNAL_ERROR
        request_id:
          type: string
          pattern: "^req_[a-f0-9]{32}$"
        details:
          type: object
          additionalProperties: true

    Error:
      type: object
      additionalProperties: false
      required: [code, message, request_id]
      properties:
        code:
          type: string
          description: |
            SIDE_EFFECT_UNCONFIRMED means an active POST or a browser action
            sequence containing click, press, or double_click may have mutated
            the target and will not be retried automatically.
          enum:
            - INVALID_REQUEST
            - UNAUTHENTICATED
            - FORBIDDEN
            - NOT_FOUND
            - CONFLICT
            - RATE_LIMITED
            - INSUFFICIENT_CREDITS
            - URL_BLOCKED
            - DNS_REBINDING
            - POLICY_DENIED
            - TIMEOUT
            - CONNECT_ERROR
            - TLS_ERROR
            - HTTP_BLOCKED
            - CHALLENGE_FAILED
            - RENDER_FAILED
            - EXTRACTION_FAILED
            - PROXY_UNAVAILABLE
            - LIMIT_EXCEEDED
            - SIDE_EFFECT_UNCONFIRMED
            - CANCELLED
            - SERVICE_UNAVAILABLE
            - INTERNAL_ERROR
        message:
          type: string
          maxLength: 1000
        request_id:
          type: string
          maxLength: 128
        details:
          type: object
          additionalProperties: true
