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

# Submit one snapshot for compression

> Submit one already-redacted snapshot to the codec service. The
response carries the compressed output the connector should splice
into its agent's tool response in place of the original `snapshot_yaml`.

For `frame_type: "pass-through"` responses, `compressed_output` is
omitted and the connector reuses the `snapshot_yaml` it sent.




## OpenAPI

````yaml /openapi.yaml post /v1/snapshot
openapi: 3.1.0
info:
  title: JD Codec Cloud API
  version: 1.0.0
  summary: Per-snapshot compression API for browser-agent perception streams.
  description: |
    JD Codec is a stateful, lossy perception codec for browser-based AI agents.
    The local connector sends already-redacted DOM/ARIA snapshots to this API
    and receives compressed output — a full keyframe on the first or forced-
    refresh step, and smaller deltas on subsequent steps — that the connector
    splices into its agent's tool response in place of the original snapshot.

    Three wire types cross the connector ↔ cloud boundary:

    - `SnapshotRequest` — one snapshot per request.
    - `SnapshotResponse` — the codec-emitted compressed output (or a
      pass-through signal indicating the connector should reuse the snapshot
      it sent).
    - `ErrorResponse` — uniform shape for every non-2xx response.

    A `UsageEvent` is also emitted server-side per successful snapshot. It is
    persisted to JDC's metering store and is not returned in the response
    body. Customers who want to consume their own usage data should query
    the dashboard at https://jdcodec.com (forthcoming customer dashboard).

    ### Privacy posture

    The connector runs an on-device Privacy Shield over every snapshot before
    it is sent. The cloud refuses any request that does not carry the
    `client_redacted: true` audit signal. Redacted values never reach the
    cloud; only category-level counts (e.g. `{ email: 2, CC_GENERIC: 1 }`)
    flow through to operators for observability.

    ### Sessions and tasks

    A **session** is the technical container for codec state. A **task** is
    a user-meaningful unit of work that fully fits inside one session. Steps
    are 0-indexed snapshots within a session and must increase monotonically.

    Sessions expire after 30 minutes of inactivity (idle TTL) and 4 hours
    absolute, whichever fires first. Per-key overrides are available — talk
    to support if your workload needs a different envelope.
  contact:
    name: JD Codec
    email: hello@jdcodec.com
    url: https://jdcodec.com
  license:
    name: Proprietary
    url: https://jdcodec.com/terms
servers:
  - url: https://api.jdcodec.com
    description: Production
  - url: https://api-staging.jdcodec.com
    description: Staging
security:
  - bearerAuth: []
tags:
  - name: Snapshot
    description: |
      Submit one snapshot, receive the codec-emitted compressed output (or a
      pass-through flag).
  - name: Telemetry
    description: |
      Submit connector-side latency measurements. Observability-only —
      never billable. The codec service is the sole writer to the
      backing telemetry store; the connector does not hold database
      credentials.
  - name: Health
    description: Liveness probes.
paths:
  /v1/snapshot:
    post:
      tags:
        - Snapshot
      summary: Submit one snapshot for compression
      description: |
        Submit one already-redacted snapshot to the codec service. The
        response carries the compressed output the connector should splice
        into its agent's tool response in place of the original `snapshot_yaml`.

        For `frame_type: "pass-through"` responses, `compressed_output` is
        omitted and the connector reuses the `snapshot_yaml` it sent.
      operationId: postSnapshot
      parameters:
        - $ref: '#/components/parameters/ApiVersionHeader'
        - $ref: '#/components/parameters/RequestIdHeaderRequest'
        - $ref: '#/components/parameters/AcceptEncodingHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SnapshotRequest'
            examples:
              standardPFrame:
                summary: Standard step inside an open session
                value:
                  session_id: 9c1b2f6e-0b8e-4a77-9cfd-3e3f7b5e8d21
                  task_id: a7d19e44-31f6-4a02-8f9b-0c2a5fbc11d3
                  step: 7
                  url: >-
                    https://example.com/admin/catalog/product/edit/id/[REDACTED_ID]
                  snapshot_yaml: |
                    - role: main
                      ref: "#main"
                      children:
                        - role: button
                          name: Save
                  client_redacted: true
                  redaction_stats:
                    email: 2
                    CC_GENERIC: 1
              firstStepSparse:
                summary: First step in a fresh session, no PII matched
                value:
                  session_id: f47ac10b-58cc-4372-a567-0e02b2c3d479
                  task_id: f47ac10b-58cc-4372-a567-0e02b2c3d479
                  step: 0
                  url: https://example.com/dashboard
                  snapshot_yaml: |
                    - role: main
                      ref: "#main"
                  client_redacted: true
                  redaction_stats: {}
              bypassUnredacted:
                summary: |
                  Privacy Shield bypass — unredacted send. Accepted only when
                  the key is provisioned for bypass; otherwise
                  `400 privacy_shield_missing`.
                value:
                  session_id: 9c1b2f6e-0b8e-4a77-9cfd-3e3f7b5e8d21
                  task_id: a7d19e44-31f6-4a02-8f9b-0c2a5fbc11d3
                  step: 3
                  url: https://example.com/admin/orders/order/view/id/100482
                  snapshot_yaml: |
                    - role: main
                      ref: "#main"
                      children:
                        - role: button
                          name: Refund
                  client_redacted: false
                  privacy_shield_bypass: true
                  redaction_stats: {}
      responses:
        '200':
          description: |
            Snapshot processed. The connector splices `compressed_output` into
            its agent's tool response, or — on `frame_type: "pass-through"` —
            reuses the `snapshot_yaml` it sent.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-JDC-Codec-Version:
              $ref: '#/components/headers/XJdcCodecVersion'
            Content-Encoding:
              $ref: '#/components/headers/ContentEncoding'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SnapshotResponse'
              examples:
                pFrame:
                  summary: Delta compression on a mid-session step
                  value:
                    frame_type: P
                    compressed_output: 'P7: [edits] main.children[4] …'
                    compression_stats:
                      input_chars: 40123
                      output_chars: 5432
                      codec_ms: 9.1
                pNoChange:
                  summary: No meaningful change since the last step
                  value:
                    frame_type: P-nochange
                    compressed_output: >
                      === No Changes (Step 5, 2 in a row) ===

                      URL /admin/catalog/products | page: 1 heading, 1 search, 8
                      selects, 23 buttons | recent: combobox "Status" [ref=e51],
                      button "Apply Filters" [ref=e58]
                    compression_stats:
                      input_chars: 40123
                      output_chars: 178
                      codec_ms: 1.4
                passThrough:
                  summary: |
                    Pass-through — codec chose not to compress. The connector
                    reuses the `snapshot_yaml` it sent. `compressed_output` is
                    omitted from the response body.
                  value:
                    frame_type: pass-through
                    compression_stats:
                      input_chars: 220148
                      output_chars: 220148
                      codec_ms: 0.4
        '400':
          description: |
            Malformed request, missing required fields, Privacy Shield audit
            signal absent (including a `client_redacted: false` send without an
            accepted bypass), or step out of order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                privacyShieldMissing:
                  $ref: '#/components/examples/PrivacyShieldMissing'
                privacyShieldStructural:
                  $ref: '#/components/examples/PrivacyShieldStructural'
                stepOutOfOrder:
                  $ref: '#/components/examples/StepOutOfOrder'
                versionUnsupported:
                  $ref: '#/components/examples/VersionUnsupported'
        '401':
          description: Bearer missing, malformed, unknown, or revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                authMissing:
                  $ref: '#/components/examples/AuthMissing'
                authInvalid:
                  $ref: '#/components/examples/AuthInvalid'
                authRevoked:
                  $ref: '#/components/examples/AuthRevoked'
        '402':
          description: Key over its configured billing cap.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                quotaExceeded:
                  $ref: '#/components/examples/QuotaExceeded'
        '410':
          description: |
            Session expired (idle or absolute TTL elapsed). What to do
            depends on which side of the wire you're on:

            - **Connector users** (most customers): the connector handles
              this transparently. It opens a fresh `session_id` and
              `task_id`, re-sends the current snapshot as step 0, and
              continues. No client action required.
            - **Direct API integrators** (custom client builders):
              generate a fresh `session_id` and `task_id` (UUID v4),
              reset `step` to 0, and re-send the current snapshot. The
              previous session's state is gone server-side and cannot
              be recovered; treat the next request as a fresh keyframe.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                sessionExpired:
                  $ref: '#/components/examples/SessionExpired'
        '413':
          description: |
            `snapshot_yaml` exceeds 2 MiB decoded. The connector should fall
            through to its degraded path and surface the already-redacted
            snapshot to the agent unchanged.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                payloadTooLarge:
                  $ref: '#/components/examples/PayloadTooLarge'
        '429':
          description: |
            Reserved for forward-compatibility. Not emitted in v1; per-key
            rate limits are not enforced today. Connectors must still handle
            this gracefully (respect `Retry-After`; exponential backoff;
            then degrade) so enabling the limit later requires no client
            update.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                rateLimited:
                  $ref: '#/components/examples/RateLimited'
        '500':
          description: Unhandled server-side exception.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                serverError:
                  $ref: '#/components/examples/ServerError'
        '503':
          description: |
            Server under load; request shed. Connector should respect
            `Retry-After`, then degrade.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                codecOverloaded:
                  $ref: '#/components/examples/CodecOverloaded'
components:
  parameters:
    ApiVersionHeader:
      name: X-JDC-API-Version
      in: header
      required: true
      description: |
        Always `1` for v1. Missing or mismatched value returns
        `400 version_unsupported`.
      schema:
        type: string
        enum:
          - '1'
        example: '1'
    RequestIdHeaderRequest:
      name: X-Request-Id
      in: header
      required: false
      description: |
        Optional client-supplied UUID v4. The server echoes this header in
        the response and in every error body. If absent or malformed, the
        server assigns one.
      schema:
        type: string
        format: uuid
    AcceptEncodingHeader:
      name: Accept-Encoding
      in: header
      required: false
      description: |
        `gzip` recommended. Snapshot bodies and compressed output are JSON
        and benefit from response compression on slow links.
      schema:
        type: string
        example: gzip
  schemas:
    SnapshotRequest:
      type: object
      required:
        - session_id
        - task_id
        - step
        - url
        - snapshot_yaml
        - client_redacted
        - redaction_stats
      properties:
        session_id:
          type: string
          format: uuid
          description: |
            Client-generated UUID v4. Stable across every snapshot in the
            session.
        task_id:
          type: string
          format: uuid
          description: |
            Client-generated UUID v4. Stable across every snapshot in a
            single task. Tasks are fully contained in one session.
        step:
          type: integer
          minimum: 0
          description: |
            0-indexed step within the session. Must increase monotonically
            within a session; out-of-order returns `400 step_out_of_order`.
        url:
          type: string
          description: |
            Page URL at the time of snapshot. On the default path it is
            **already redacted** by the connector's Privacy Shield — values
            such as IDs, tokens, and other PII-shaped substrings are replaced
            with bracketed tokens. Under an accepted bypass
            (`client_redacted: false` + `privacy_shield_bypass: true`) it is
            the **raw** URL; the service structurally minimizes it before
            persisting (host + path + query parameter names; query values,
            fragment, and userinfo dropped).
        snapshot_yaml:
          type: string
          maxLength: 2097152
          description: |
            Playwright-MCP YAML snapshot, UTF-8 encoded. **Already redacted**
            on the default path; **raw** under an accepted bypass. Maximum
            decoded size 2 MiB; larger snapshots return `413 payload_too_large`.
        client_redacted:
          type: boolean
          description: |
            Audit signal that the connector's on-device Privacy Shield has
            run. **`true` on the default path** — missing or `false` returns
            `400 privacy_shield_missing` unless the bypass conditions below
            are met.

            **`false` is accepted only under the Privacy Shield bypass**
            (D018): the request must also carry `privacy_shield_bypass: true`
            **and** the authenticated key must be provisioned with
            `privacy_shield_bypass_allowed: true`. Either missing returns
            `400 privacy_shield_missing`. See the `bypassUnredacted` example.
        privacy_shield_bypass:
          type: boolean
          description: |
            Optional. Set `true` alongside `client_redacted: false` to opt
            into an unredacted send. Additive — omitting it (the default
            path) is unchanged behaviour. Accepted only when the
            authenticated key is provisioned for bypass; otherwise
            `400 privacy_shield_missing`. When `true`, `redaction_stats`
            is `{}` and both `url` and `snapshot_yaml` are raw.
        redaction_stats:
          $ref: '#/components/schemas/RedactionStats'
        agent_llm:
          $ref: '#/components/schemas/AgentLlm'
          description: |
            Optional. Customer's agent-LLM metadata, announced on the first
            snapshot of a session; ignored on subsequent snapshots of the
            same session (the codec service stores it on first sight).

            Used to pick the correct tokenizer for token-aware features.
            When absent, the codec falls back to `tiktoken-fallback` with
            approximate counts. Char counts are unaffected by this field.
    SnapshotResponse:
      type: object
      required:
        - frame_type
        - compression_stats
      properties:
        frame_type:
          $ref: '#/components/schemas/SnapshotFrameType'
        compressed_output:
          type: string
          description: |
            The codec-emitted replacement for `snapshot_yaml`.

            - **Required** when `frame_type` is `I`, `P`, or `P-nochange`.
            - **Absent** when `frame_type` is `pass-through` — the
              connector reuses the `snapshot_yaml` it sent in the request.
        compression_stats:
          $ref: '#/components/schemas/CompressionStats'
        token_stats:
          $ref: '#/components/schemas/TokenStats'
          description: |
            Optional. Absent in the v1 steady-state path because
            tokenization runs asynchronously after the response is sent.
            See `TokenStats` for the full lifecycle rationale.

            The authoritative per-snapshot token data lives on the
            `usage_events` row keyed by `(session_id, step)`; the connector
            can fetch it later via the usage-report endpoint.
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorBody'
    RedactionStats:
      type: object
      additionalProperties:
        type: integer
        minimum: 0
      description: |
        Category → count map. Empty `{}` means the Privacy Shield ran and
        matched nothing. **Counts only — never the redacted values.**
      examples:
        - {}
        - email: 2
          CC_GENERIC: 1
        - phone: 1
          address: 4
    AgentLlm:
      type: object
      required:
        - provider
      description: |
        Downstream LLM provider the customer's agent calls. The codec
        service uses `provider` to pick the tokenizer family and `model`
        to refine the tokenizer version (e.g. Anthropic Opus 4.7 uses a
        different tokenizer than Sonnet 4.6 — up to 35% more tokens on
        the same text per Anthropic's pricing documentation).
      properties:
        provider:
          $ref: '#/components/schemas/LlmProvider'
        model:
          type: string
          description: |
            Optional. Example values: `claude-sonnet-4-6`, `gpt-4o`,
            `gemini-2.5-pro`. Refines tokenizer selection when the
            provider supports multiple tokenizer versions.
    SnapshotFrameType:
      type: string
      enum:
        - I
        - P
        - P-nochange
        - pass-through
      description: |
        Frame type emitted by the codec. The single-letter values are the
        wire encoding; the human-readable meaning of each:

        - `I` — keyframe. Full snapshot, returned on the first step of a
          session and on forced refreshes (e.g. URL change). Equivalent in
          size to the original snapshot.
        - `P` — delta. Only what changed since the previous step. Typically
          a small fraction of the snapshot size.
        - `P-nochange` — no meaningful change since the previous step. The
          response carries a sentinel header (`=== No Changes (Step N) ===`,
          with an optional `, K in a row` suffix on consecutive nochanges)
          plus a single short recap line summarising the URL, the
          actionable-now element counts on the page, and up to three
          recently-changed elements as a small ref anchor. The recap helps
          an agent stay oriented across a stretch of no-op turns without
          requesting a fresh snapshot.
        - `pass-through` — the codec chose not to compress this snapshot
          (e.g. a full-table scan where compression wouldn't help). The
          connector reuses the `snapshot_yaml` it sent in the request.
          `compressed_output` is omitted from the response body.
    CompressionStats:
      type: object
      required:
        - input_chars
        - output_chars
        - codec_ms
      properties:
        input_chars:
          type: integer
          minimum: 0
          description: Character count of the submitted `snapshot_yaml`.
        output_chars:
          type: integer
          minimum: 0
          description: |
            Character count of the emitted output. On pass-through, equals
            `input_chars`.
        codec_ms:
          type: number
          minimum: 0
          description: |
            Milliseconds spent inside the codec pipeline. On pass-through,
            this is detection cost only.
    TokenStats:
      type: object
      required:
        - token_status
      description: |
        Token-side counterpart of `CompressionStats`. Optional on the
        response because tokenization runs asynchronously after the
        response is sent — the block is absent (or
        `token_status: "pending"`) in the steady-state v1 path, present
        when the tokenizer happened to complete inline.

        `chars` (on `CompressionStats`) are deterministic, sync ground
        truth at the codec boundary. `tokens` are a downstream measurement
        that may legitimately be missing. Consumers MUST check
        `token_status` before reading any token field — fields are absent
        on `"pending"` and `"failed"` rows. On `"failed"`, render chars as
        fallback context and surface "tokens unavailable" — never an
        approximate substitute.
      properties:
        token_status:
          $ref: '#/components/schemas/TokenStatus'
        input_tokens:
          type: integer
          minimum: 0
          description: |
            Provider-tokenizer count of the inbound snapshot. Only
            meaningful when `token_status` is `"ok"`.
        output_tokens:
          type: integer
          minimum: 0
          description: |
            Provider-tokenizer count of the codec-emitted output. Only
            meaningful when `token_status` is `"ok"`.
        token_source:
          $ref: '#/components/schemas/TokenSource'
        token_source_version:
          type: string
          description: |
            Opaque per-source version string used for cross-batch
            comparability and to flag rows whose counts came from a
            tokenizer we no longer use.

            - API-based tokenizers (Anthropic `count_tokens` endpoint)
              record the month the call was made, e.g.
              `anthropic-api@2026-05`. The API is opaque, so this is
              the finest granularity we can pin from outside;
              cross-month drift gets caught by CI golden fixtures.
            - Library-based tokenizers record the exact package version,
              e.g. `tiktoken@0.7.0` or `@google/generative-ai@0.21.0`.
          example: anthropic-api@2026-05
        cache_read_input_tokens:
          type: integer
          minimum: 0
          description: |
            Anthropic-specific. Tokens served from prompt cache on this
            snapshot's LLM call. Absent when `token_source` is not
            `"anthropic"` or when the customer's agent did not pass
            `cache_control`. Cache reads bill at 0.1x the base input rate.
        cache_creation_input_tokens:
          type: integer
          minimum: 0
          description: |
            Anthropic-specific. Tokens written to prompt cache on this
            snapshot's LLM call. Absent when `token_source` is not
            `"anthropic"` or when the customer's agent did not pass
            `cache_control`. Cache writes bill at 1.25x or 2x the base
            input rate depending on TTL.
        cached_content_tokens:
          type: integer
          minimum: 0
          description: |
            Gemini-specific. Tokens served from Gemini's cached-content
            mechanism on this snapshot's LLM call. Absent when
            `token_source` is not `"gemini"`.
    ErrorBody:
      type: object
      required:
        - code
        - message
        - request_id
      properties:
        code:
          $ref: '#/components/schemas/ErrorCode'
        message:
          type: string
          description: |
            Human-readable summary. Stable category-level wording; never
            includes algorithm names, internal state, or any portion of
            the submitted snapshot.
        request_id:
          type: string
          format: uuid
          description: |
            Echoes `X-Request-Id` for support correlation.
    LlmProvider:
      type: string
      enum:
        - anthropic
        - openai
        - gemini
      description: |
        Customer's downstream LLM provider. Mirrors `TokenSource` minus
        `tiktoken-fallback` — customers never request the fallback
        tokenizer; the server uses it when no `agent_llm` is announced.

        For customers using aggregators (Bedrock, OpenRouter, Vertex, Azure,
        Together, Fireworks), set this to the *underlying model's*
        tokenizer family — the aggregator does not tokenize, the model
        does. Claude on Bedrock or Vertex → `anthropic`; OpenAI on Azure
        → `openai`; etc.
    TokenStatus:
      type: string
      enum:
        - pending
        - ok
        - failed
      description: |
        Lifecycle of the `token_stats` block.

        - `pending` — async tokenization in flight; token-count fields
          absent.
        - `ok` — tokenization succeeded; token-count fields populated.
        - `failed` — tokenization failed terminally; token-count fields
          absent. Customer-facing tooling renders chars as fallback
          context and surfaces "tokens unavailable for this snapshot" —
          never an approximate substitute. Honesty over completeness.
    TokenSource:
      type: string
      enum:
        - anthropic
        - openai
        - gemini
        - tiktoken-fallback
      description: |
        Which tokenizer produced the token counts on this row.

        `tiktoken-fallback` is `cl100k_base` and is used when the
        customer's agent-LLM provider is unset, unsupported, or unknown.
        `cl100k_base` is an OpenAI tokenizer; for Anthropic / Gemini
        calls its counts are an estimate (~22% under-count vs Anthropic
        billing on codec-style content), not an invoice number.
        Customer-facing tooling must surface "approximate" when this is
        the source.
    ErrorCode:
      type: string
      enum:
        - version_unsupported
        - malformed_request
        - privacy_shield_missing
        - privacy_shield_structural
        - privacy_shield_violation
        - step_out_of_order
        - auth_missing
        - auth_invalid
        - auth_revoked
        - quota_exceeded
        - session_expired
        - payload_too_large
        - rate_limited
        - server_error
        - codec_overloaded
        - telemetry_value_invalid
        - telemetry_session_unknown
        - telemetry_too_late
      description: |
        Stable machine-readable error tokens. Clients may branch on these
        values. New codes are additive within v1 — existing codes never
        change meaning.

        Reserved (defined for forward-compatibility, not emitted in v1):

        - `privacy_shield_violation` — placeholder for a future server-side
          secondary check. Defined now so enabling it later requires no
          connector update.
        - `rate_limited` — per-key rate limiting is not enforced in v1.
          Connectors must still handle `429` gracefully so the limit can
          be enabled server-side without client work.

        Telemetry-specific (emitted only by the `/v1/telemetry`
        endpoint):

        - `telemetry_value_invalid` (400) — a timing value was negative,
          non-numeric, or above the 600 000 ms (10 minute) ceiling.
          Connector should log + drop, not retry.
        - `telemetry_session_unknown` (404) — the `session_id` is unknown
          or not owned by this bearer. Connector should log + drop.
        - `telemetry_too_late` (410) — session has expired (TTL) and the
          telemetry row would have no useful join target. Connector
          drops the data.
  headers:
    XRequestId:
      description: |
        Echoed from `X-Request-Id` on the request, or assigned by the server
        when absent. Quote this in support requests.
      schema:
        type: string
        format: uuid
    XJdcCodecVersion:
      description: |
        Codec build identifier. Format `YYYY.MM.DD+<short-sha>`. Matches the
        `codec_version` field in the corresponding `UsageEvent`.
      schema:
        type: string
        example: 2026.05.01+a1b2c3d
    ContentEncoding:
      description: |
        `gzip` when negotiated via `Accept-Encoding`. Absent otherwise.
      schema:
        type: string
        example: gzip
  examples:
    PrivacyShieldMissing:
      summary: Privacy Shield audit signal absent
      value:
        error:
          code: privacy_shield_missing
          message: Request missing client_redacted=true audit signal.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    PrivacyShieldStructural:
      summary: Privacy Shield payload present but mis-shaped
      value:
        error:
          code: privacy_shield_structural
          message: redaction_stats must be an object of non-negative integer counts.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    StepOutOfOrder:
      summary: Step number not monotonically increasing
      value:
        error:
          code: step_out_of_order
          message: step must increase monotonically within a session.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    VersionUnsupported:
      summary: X-JDC-API-Version header missing or not accepted
      value:
        error:
          code: version_unsupported
          message: X-JDC-API-Version header missing or not accepted.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    AuthMissing:
      summary: Authorization header absent
      value:
        error:
          code: auth_missing
          message: Missing Authorization header. Set JDC_API_KEY in your environment.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    AuthInvalid:
      summary: Unknown key id, or wrong secret
      value:
        error:
          code: auth_invalid
          message: Authorization rejected. Regenerate your API key.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    AuthRevoked:
      summary: Key row exists but has been revoked
      value:
        error:
          code: auth_revoked
          message: API key has been revoked. Issue a new key.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    QuotaExceeded:
      summary: Key over its configured billing cap
      value:
        error:
          code: quota_exceeded
          message: API key over its configured billing cap.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    SessionExpired:
      summary: Session idle or absolute TTL elapsed
      value:
        error:
          code: session_expired
          message: Session idle or absolute TTL elapsed. Start a new session_id.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    PayloadTooLarge:
      summary: snapshot_yaml exceeds 2 MiB
      value:
        error:
          code: payload_too_large
          message: snapshot_yaml exceeds 2 MiB. Fall through to the degraded path.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    RateLimited:
      summary: Reserved for forward-compatibility
      value:
        error:
          code: rate_limited
          message: Request rate-limited. Respect Retry-After and back off.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    ServerError:
      summary: Unhandled server-side exception
      value:
        error:
          code: server_error
          message: Internal server error. Retry once with jitter, then degrade.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
    CodecOverloaded:
      summary: Server under load; request shed
      value:
        error:
          code: codec_overloaded
          message: Codec service overloaded. Respect Retry-After, then degrade.
          request_id: 7c2d1e44-9f63-4801-b8e4-2a2bba31c019
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: <api_key_id>.<api_key_secret>
      description: |
        API key in two parts separated by a dot, sent as a single bearer:

        - `api_key_id` — public, stable, `jdck_`-prefixed lookup handle.
          Safe to log and reference in support conversations.
        - `api_key_secret` — opaque high-entropy string. Shown to the
          customer once at issuance and never again. Server stores only
          its hash.

        Example:

            Authorization: Bearer jdck_9f3a2b7c8d1e4f05.BASE32URL_SECRET

````