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

# Spend what a quote priced, as one job group

> Turns a recorded quote into a job group: one member per quoted entry, each either a transcription job reserved at the entry's ceiling or, for an entry priced as cached, a settled read at the price the quote gave it. This is the operation that spends.

**Every guard runs before the first credit moves.** The quote must be the caller's own and unexpired (`quote_not_found`, `quote_expired`); an `expected_total_credits` in the body must agree with the quote's recorded `total_ceiling_credits`, checked before anything is probed (`expected_total_mismatch`); every entry's enclosure is re-probed and a republished one refuses the whole confirm naming the episode (`quote_mismatch` — take a new quote), while entries the probe budget did not reach refuse it unverified (`quote_unverified` — send the same confirm again); the group's job-bearing members are checked once against the tier's open-job cap (`concurrency_limited`, naming the cap and the room left) and the whole group's ceiling against the balance (`payment_required`). A refusal here writes nothing at all.

**`Idempotency-Key` is required.** The group is created under it before any member is reserved, so a repeat with the same key and body returns the original group — whatever state it is in — with `200` and charges nothing further. The same key with a different body is `409 idempotency_conflict`.

**A confirm that dies half way is recoverable without you.** The group is written `pending` first and marked `complete` only when every member has landed; a group still pending past its `completion_deadline` is swept — its reservations returned, its status `abandoned`. Reading the group tells you which.



## OpenAPI

````yaml /openapi.yaml post /v1/quotes/{quote_id}/confirm
openapi: 3.1.0
info:
  title: Audivo API
  version: 0.11.0
  description: >-
    The Audivo API turns a public podcast episode into a clean, timed
    transcript. Name an episode by an Apple Podcasts link, by its RSS feed URL
    and GUID, or by the episode id an earlier answer gave you; get back
    structured, timed text — at once for an episode already transcribed, or as a
    job you poll for fresh audio.


    Two authentication schemes, and which an operation takes is part of what it
    is: `apiKeyBearer` — an `hk_live_` key sent as `Authorization: Bearer` —
    authenticates the public API, and `dashboardJwt` authenticates the
    dashboard's own operations. No operation that takes one accepts the other in
    its place. An API-key caller reads its balance and reservations from any
    quote response.


    Deferred on purpose, and absent from this document rather than hidden:
    webhooks, subscriptions, speaker labels beyond the frozen `diarized: false`
    shape, custom vocabulary, caller-selected engines, raw-audio URLs and
    uploads, SDKs, translation, and cross-corpus search. What changed in each
    version is on the documentation site's changelog.
  license:
    name: Proprietary
    url: https://docs.audivo.dev/legal/terms
servers:
  - url: https://k8mfogcvz4.execute-api.us-east-1.amazonaws.com/prod
    description: Production API. Append the operation path, including /v1.
security: []
tags:
  - name: Transcripts
    description: Create, poll, retrieve, and cancel transcription jobs.
  - name: Catalog
    description: >-
      Public podcast show/episode search, charts, detail, and resolution. Every
      metadata/search/list call in this tag costs 1 credit; no catalog response
      echoes `credits_charged`, so the charge is stated here rather than
      per-response. Not yet applied: the discovery operations shipped in 0.2.0
      (`GET /v1/search/shows`, `GET /v1/charts`) are metered by request rate
      only and write no credit-bearing row; the per-call charge lands with the
      catalog reads that bill.
  - name: Quotes
    description: >-
      Price a selection before spending. A quote resolves every episode in it,
      records what it resolved under a short TTL, and reserves nothing; the
      confirm that follows is the operation that spends, and it answers with a
      job group.
  - name: Groups
    description: >-
      The job group: what one confirm created. A group holds N ≥ 1 members, each
      a transcription job or a settled cache read; a single-episode `POST
      /v1/transcripts` is a group of one. A caller reads a group's rollup, lists
      their own groups newest first, and cancels a group, which cancels every
      member not yet started and releases each reservation exactly once.
  - name: Session
    description: >-
      Dashboard-JWT session establishment. One operation, called by the
      dashboard's backend-for-frontend whenever it turns a Cognito sign-in into
      a Audivo session, and the one place a caller who does not yet have an
      account is served.
  - name: Account
    description: Usage, limits, and supported languages for the calling account.
  - name: API Keys
    description: Dashboard-JWT key lifecycle management.
  - name: Billing
    description: Dashboard-JWT Stripe Checkout, Customer Portal, invoices, and refunds.
  - name: Data
    description: Dashboard-JWT portable export and account deletion.
paths:
  /v1/quotes/{quote_id}/confirm:
    post:
      tags:
        - Quotes
      summary: Spend what a quote priced, as one job group
      description: >-
        Turns a recorded quote into a job group: one member per quoted entry,
        each either a transcription job reserved at the entry's ceiling or, for
        an entry priced as cached, a settled read at the price the quote gave
        it. This is the operation that spends.


        **Every guard runs before the first credit moves.** The quote must be
        the caller's own and unexpired (`quote_not_found`, `quote_expired`); an
        `expected_total_credits` in the body must agree with the quote's
        recorded `total_ceiling_credits`, checked before anything is probed
        (`expected_total_mismatch`); every entry's enclosure is re-probed and a
        republished one refuses the whole confirm naming the episode
        (`quote_mismatch` — take a new quote), while entries the probe budget
        did not reach refuse it unverified (`quote_unverified` — send the same
        confirm again); the group's job-bearing members are checked once against
        the tier's open-job cap (`concurrency_limited`, naming the cap and the
        room left) and the whole group's ceiling against the balance
        (`payment_required`). A refusal here writes nothing at all.


        **`Idempotency-Key` is required.** The group is created under it before
        any member is reserved, so a repeat with the same key and body returns
        the original group — whatever state it is in — with `200` and charges
        nothing further. The same key with a different body is `409
        idempotency_conflict`.


        **A confirm that dies half way is recoverable without you.** The group
        is written `pending` first and marked `complete` only when every member
        has landed; a group still pending past its `completion_deadline` is
        swept — its reservations returned, its status `abandoned`. Reading the
        group tells you which.
      operationId: confirmQuote
      parameters:
        - $ref: '#/components/parameters/QuoteIdPathParam'
        - $ref: '#/components/parameters/RequiredIdempotencyKeyHeader'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConfirmRequest'
            examples:
              empty:
                summary: Nothing to say — the quote id is in the path
                value: {}
              fenced:
                summary: >-
                  The quote's total restated, which the API refuses to spend
                  past
                value:
                  expected_total_credits: 232
      responses:
        '200':
          description: >-
            The job group this confirm created, complete, with every member
            named — or, on a repeat under the same `Idempotency-Key`, the
            original group as it stands.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobGroupResponse'
              examples:
                confirmed:
                  $ref: '#/components/examples/ConfirmedGroup'
        '400':
          $ref: '#/components/responses/BadRequestApiKey'
        '401':
          $ref: '#/components/responses/UnauthorizedApiKey'
        '402':
          $ref: '#/components/responses/PaymentRequiredApiKey'
        '404':
          $ref: '#/components/responses/QuoteNotFoundApiKey'
        '409':
          $ref: '#/components/responses/ConflictApiKeyConfirm'
        '429':
          $ref: '#/components/responses/TooManyRequestsApiKey'
        '500':
          $ref: '#/components/responses/InternalErrorApiKey'
      security:
        - apiKeyBearer: []
components:
  parameters:
    QuoteIdPathParam:
      name: quote_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/QuoteId'
    RequiredIdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      required: true
      description: >-
        As `IdempotencyKeyHeader`, but required: a confirm spends up to a whole
        selection's ceiling, and a retry that could not be recognized as one
        would spend it twice. Missing is `400 invalid_request`.
      schema:
        type: string
        minLength: 1
        maxLength: 255
        pattern: ^[!-~]{1,255}$
  schemas:
    ConfirmRequest:
      type: object
      additionalProperties: false
      description: >-
        The quote is in the path and the key is in the header, so the body
        carries at most the spending fence below. An empty object, or no body at
        all, is accepted; any other property is refused, so a further field can
        be added without an old client having silently sent it.
      properties:
        expected_total_credits:
          $ref: '#/components/schemas/CreditAmount'
          description: >-
            The quote's `total_ceiling_credits` as the caller read it. Sent, and
            disagreeing with the quote's own total, the API refuses the confirm
            with `409 expected_total_mismatch` before an enclosure is probed or
            a credit is reserved: a caller that has misread the price cannot
            spend it. Omitted, the confirm proceeds on the quote's recorded
            total alone.
    JobGroupResponse:
      type: object
      additionalProperties: false
      required:
        - group_id
        - status
        - quote_id
        - member_count
        - members
        - member_counts
        - credits_reserved
        - credits_settled
        - credits_released
        - created_at
        - completion_deadline
        - completed_at
        - abandoned_at
      description: >-
        One group's rollup. `member_count` is how many members the confirm set
        out to create; `members` is how many exist, which is fewer only while
        the group is `pending` (or after it was `abandoned` part way).
        `credits_reserved` is what the group still holds against the balance —
        the reservations of members not yet terminal; `credits_settled` is what
        it has been charged, job members and cache reads together;
        `credits_released` is what came back from members that failed, were
        cancelled, or settled below their ceiling. None of the three is a bill:
        each job member carries its own figures, and the ledger is the record.
      properties:
        group_id:
          $ref: '#/components/schemas/GroupId'
        status:
          $ref: '#/components/schemas/JobGroupStatus'
        quote_id:
          description: >-
            The quote this group confirmed, or `null` for a single-episode
            submission.
          anyOf:
            - $ref: '#/components/schemas/QuoteId'
            - enum:
                - null
        member_count:
          type: integer
          minimum: 1
        members:
          type: array
          maxItems: 100
          items:
            $ref: '#/components/schemas/JobGroupMember'
        member_counts:
          $ref: '#/components/schemas/JobGroupMemberCounts'
        credits_reserved:
          $ref: '#/components/schemas/CreditAmount'
        credits_settled:
          $ref: '#/components/schemas/CreditAmount'
        credits_released:
          $ref: '#/components/schemas/CreditAmount'
        created_at:
          type: string
          format: date-time
        completion_deadline:
          type: string
          format: date-time
          description: Past this, a still-`pending` group is swept.
        completed_at:
          $ref: '#/components/schemas/NullableDateTime'
        abandoned_at:
          $ref: '#/components/schemas/NullableDateTime'
    QuoteId:
      type: string
      pattern: ^qte_[A-Za-z0-9]{16,32}$
      maxLength: 40
      description: Opaque, server-generated quote identifier.
    CreditAmount:
      type: integer
      minimum: 0
      maximum: 9007199254740991
      description: >-
        A non-negative JavaScript-safe integer credit amount. The upper bound
        matches `Number.MAX_SAFE_INTEGER`, which is enforced by the domain
        ledger so arithmetic and JSON round-trips cannot silently lose cents
        worth of credit precision.
    GroupId:
      type: string
      pattern: ^grp_[A-Za-z0-9]{16,32}$
      maxLength: 40
      description: Opaque, server-generated job group identifier.
    JobGroupStatus:
      type: string
      enum:
        - pending
        - complete
        - abandoned
      description: >-
        `pending` while a confirm is still fanning out (or died doing so);
        `complete` once every member landed; `abandoned` once the sweeper
        returned a pending group's reservations past its deadline. A cancelled
        group is `complete` with `cancelled` members.
    JobGroupMember:
      description: >-
        One member of a group — a job, or a settled cache read. `kind`
        discriminates.
      oneOf:
        - $ref: '#/components/schemas/JobGroupJobMember'
        - $ref: '#/components/schemas/JobGroupCachedReadMember'
    JobGroupMemberCounts:
      type: object
      additionalProperties: false
      required:
        - validating
        - queued
        - downloading
        - transcribing
        - merging
        - completed
        - failed
        - cancelled
        - cached_read
      description: >-
        How many members are in each job state, plus how many were settled cache
        reads (which have no state — they were delivered at confirm). The nine
        sum to `member_count` on a complete group.
      properties:
        validating:
          type: integer
          minimum: 0
        queued:
          type: integer
          minimum: 0
        downloading:
          type: integer
          minimum: 0
        transcribing:
          type: integer
          minimum: 0
        merging:
          type: integer
          minimum: 0
        completed:
          type: integer
          minimum: 0
        failed:
          type: integer
          minimum: 0
        cancelled:
          type: integer
          minimum: 0
        cached_read:
          type: integer
          minimum: 0
    NullableDateTime:
      description: An ISO 8601 timestamp, or `null`.
      anyOf:
        - type: string
          format: date-time
        - enum:
            - null
    Error:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorDetail'
    JobGroupJobMember:
      type: object
      additionalProperties: false
      required:
        - kind
        - job_id
        - episode_id
        - status
        - estimated_credits
        - reserved_credits
        - created_at
      description: >-
        A member with a job behind it. `reserved_credits` is the ceiling the
        confirm reserved; `settled_credits` and `released_credits` appear as the
        job's terminal state sets them, as on `JobStatus`. Poll or cancel the
        job itself through `/v1/transcripts/{job_id}`.
      properties:
        kind:
          const: job
        job_id:
          $ref: '#/components/schemas/JobId'
        episode_id:
          $ref: '#/components/schemas/EpisodeId'
        status:
          $ref: '#/components/schemas/JobState'
        estimated_credits:
          $ref: '#/components/schemas/CreditAmount'
        reserved_credits:
          $ref: '#/components/schemas/CreditAmount'
        settled_credits:
          $ref: '#/components/schemas/CreditAmount'
        released_credits:
          $ref: '#/components/schemas/CreditAmount'
        created_at:
          type: string
          format: date-time
    JobGroupCachedReadMember:
      type: object
      additionalProperties: false
      required:
        - kind
        - read_id
        - episode_id
        - credits_charged
        - created_at
      description: >-
        A member the confirm found already transcribed: charged
        `credits_charged` (the cached-read price for this account) at confirm,
        with no job behind it. `read_id` is the job-shaped id the read was
        charged under — the id its ledger event and access record carry — so
        every credit a group spent is explainable from the group alone, and it
        is the handle its transcript is fetched by: `GET /v1/reads/{read_id}`
        delivers it, in any format, and charges nothing further. The job poll
        cannot, because this member has no job.
      properties:
        kind:
          const: cached_read
        read_id:
          $ref: '#/components/schemas/JobId'
        episode_id:
          $ref: '#/components/schemas/EpisodeId'
        credits_charged:
          $ref: '#/components/schemas/CreditAmount'
        created_at:
          type: string
          format: date-time
    ErrorDetail:
      type: object
      additionalProperties: false
      required:
        - type
        - code
        - message
        - doc_url
        - request_id
        - retryable
      properties:
        type:
          $ref: '#/components/schemas/ErrorType'
        code:
          $ref: '#/components/schemas/ErrorCode'
        message:
          type: string
          minLength: 1
          maxLength: 1000
        doc_url:
          type: string
          format: uri
          maxLength: 300
        request_id:
          $ref: '#/components/schemas/RequestId'
        retryable:
          type: boolean
      oneOf:
        - required:
            - code
            - type
          properties:
            code:
              enum:
                - invalid_request
                - invalid_url
                - source_not_supported
                - unsafe_source
            type:
              const: invalid_request
        - required:
            - code
            - type
          properties:
            code:
              enum:
                - duration_exceeded
                - size_exceeded
                - unsupported_codec
                - unsupported_language
                - credits_not_refundable
            type:
              const: unprocessable_input
        - required:
            - code
            - type
          properties:
            code:
              const: unauthenticated
            type:
              const: unauthenticated
        - required:
            - code
            - type
          properties:
            code:
              enum:
                - idempotency_conflict
                - job_not_completed
                - quote_expired
                - quote_mismatch
                - quote_unverified
                - expected_total_mismatch
                - request_in_progress
                - account_suspended
                - account_closed
                - account_not_found
                - api_key_limit_reached
                - tier_unchanged
            type:
              const: conflict
        - required:
            - code
            - type
          properties:
            code:
              enum:
                - feed_dead
                - episode_not_found
                - show_not_found
                - job_not_found
                - quote_not_found
                - group_not_found
                - api_key_not_found
                - credit_lot_not_found
            type:
              const: not_found
        - required:
            - code
            - type
          properties:
            code:
              const: content_blocked
            type:
              const: content_blocked
        - required:
            - code
            - type
          properties:
            code:
              const: payment_required
            type:
              const: payment_required
        - required:
            - code
            - type
          properties:
            code:
              enum:
                - rate_limited
                - concurrency_limited
            type:
              const: rate_limited
        - required:
            - code
            - type
          properties:
            code:
              enum:
                - engine_unavailable
                - processing_failed
                - discovery_unavailable
                - internal_error
            type:
              const: unavailable
        - required:
            - code
            - type
          properties:
            code:
              const: nothing_to_quote
            type:
              const: unprocessable_input
    JobId:
      type: string
      pattern: ^job_[A-Za-z0-9]{16,32}$
      maxLength: 40
      description: Opaque, server-generated job identifier.
    EpisodeId:
      type: string
      pattern: ^ep_[a-z2-7]{16}$
      description: Canonical episode identifier.
    JobState:
      type: string
      enum:
        - validating
        - queued
        - downloading
        - transcribing
        - merging
        - completed
        - failed
        - cancelled
      description: >-
        Legal transitions: validating → queued|failed|cancelled; queued →
        downloading|failed|cancelled; downloading → transcribing|failed;
        transcribing → merging|failed; merging → completed|failed. Terminal
        states (completed, failed, cancelled) have no further transitions.
        Cancellation is legal only from validating/queued; every non-terminal
        state may fail.
    ErrorType:
      type: string
      enum:
        - invalid_request
        - unprocessable_input
        - unauthenticated
        - conflict
        - not_found
        - content_blocked
        - payment_required
        - rate_limited
        - unavailable
      description: >-
        The family an error belongs to, for handling; `code` is the precise
        reason. `invalid_request`: something in the request is wrong — fix the
        integration. `unprocessable_input`: the request is well-formed but the
        episode or resource is outside published support. `unauthenticated`:
        missing, malformed, revoked or expired credential. `payment_required`:
        the account's credits cannot cover the ceiling. `conflict`: the
        resource's or the account's state refuses this request as sent — a
        lapsed quote, a reused idempotency key, a held account. `not_found`: the
        job, quote, group or key the request named does not exist for this
        account. `rate_limited`: over a plan limit; wait and retry.
        `unavailable`: a failure on our side or at a provider; retry, and quote
        `request_id` if it persists.
    ErrorCode:
      type: string
      enum:
        - invalid_request
        - invalid_url
        - unauthenticated
        - idempotency_conflict
        - job_not_completed
        - source_not_supported
        - feed_dead
        - episode_not_found
        - show_not_found
        - unsafe_source
        - unsupported_codec
        - unsupported_language
        - duration_exceeded
        - size_exceeded
        - content_blocked
        - payment_required
        - rate_limited
        - concurrency_limited
        - job_not_found
        - api_key_not_found
        - api_key_limit_reached
        - credit_lot_not_found
        - credits_not_refundable
        - engine_unavailable
        - processing_failed
        - discovery_unavailable
        - nothing_to_quote
        - internal_error
        - quote_not_found
        - group_not_found
        - quote_expired
        - quote_mismatch
        - quote_unverified
        - expected_total_mismatch
        - request_in_progress
        - account_suspended
        - account_closed
        - account_not_found
        - tier_unchanged
      description: >-
        Every code the API returns, and the HTTP status it comes with.
        `invalid_request`, `invalid_url` → 400. `unauthenticated` → 401.
        `payment_required` → 402. `job_not_found`, `quote_not_found`,
        `group_not_found`, `api_key_not_found`, `credit_lot_not_found` → 404.
        `idempotency_conflict`, `request_in_progress`, `job_not_completed`,
        `quote_expired`, `quote_mismatch`, `quote_unverified`,
        `expected_total_mismatch`, `account_suspended`, `account_closed`,
        `account_not_found`, `api_key_limit_reached`, `tier_unchanged` → 409.
        `source_not_supported`, `feed_dead`, `episode_not_found`,
        `show_not_found`, `unsafe_source`, `unsupported_codec`,
        `unsupported_language`, `duration_exceeded`, `size_exceeded`,
        `nothing_to_quote`, `credits_not_refundable` → 422. `content_blocked` →
        451. `rate_limited`, `concurrency_limited` → 429. `internal_error`,
        `processing_failed` → 500. `discovery_unavailable`, `engine_unavailable`
        → 503. `processing_failed` also appears inside a failed job's `error`
        field rather than as a live status. Each code has a heading on the
        documentation site's errors page, which is what `doc_url` links to.
    RequestId:
      type: string
      pattern: ^req_[A-Za-z0-9-]+$
      maxLength: 100
  headers:
    XRequestId:
      description: >-
        Echoed on every response, success or error. A refusal generated at the
        edge (401, 429, and the unmetered-key 500) carries API Gateway's own
        hyphenated request id after the `req_` prefix; a response from a
        function carries the same id without hyphens.
      schema:
        type: string
        pattern: ^req_[A-Za-z0-9-]+$
        maxLength: 100
    XRateLimitLimit:
      description: >-
        The caller's requests-per-minute ceiling for the current tier. Present
        on every response for a resolved caller, success and error alike; absent
        from `401 unauthenticated`, which has no account to report a ceiling
        for, and from the edge's own `429`, which is emitted before any function
        runs.
      schema:
        type: integer
        minimum: 0
    RetryAfter:
      description: >-
        Seconds to wait before retrying. Sent with the edge's `rate_limited`
        throttle refusal. Not sent with `concurrency_limited`, which clears when
        one of the account's own jobs reaches a terminal state — a moment the
        API cannot put a number on.
      schema:
        type: integer
        minimum: 0
  examples:
    ConfirmedGroup:
      summary: The QuoteExample confirmed — two jobs queued, one cached read settled
      value:
        group_id: grp_9k2fA7bQ3xzM1LpN
        status: complete
        quote_id: qte_9k2fA7bQ3xzM1LpN
        member_count: 3
        members:
          - kind: cached_read
            read_id: job_2c1d4e9fA7bQ3xzM
            episode_id: ep_rwg4do2imnjyhaj7
            credits_charged: 1
            created_at: '2026-09-08T09:01:00Z'
          - kind: job
            job_id: job_4e9f2c1dB8cR4yaN
            episode_id: ep_3iicyxg6ymbv75gy
            status: queued
            estimated_credits: 60
            reserved_credits: 75
            created_at: '2026-09-08T09:01:00Z'
          - kind: job
            job_id: job_9f2c1d4eC9dS5zbP
            episode_id: ep_uy4pqvjsmeyrhavz
            status: queued
            estimated_credits: 45
            reserved_credits: 57
            created_at: '2026-09-08T09:01:00Z'
        member_counts:
          validating: 0
          queued: 2
          downloading: 0
          transcribing: 0
          merging: 0
          completed: 0
          failed: 0
          cancelled: 0
          cached_read: 1
        credits_reserved: 132
        credits_settled: 1
        credits_released: 0
        created_at: '2026-09-08T09:01:00Z'
        completion_deadline: '2026-09-08T09:06:00Z'
        completed_at: '2026-09-08T09:01:00Z'
        abandoned_at: null
  responses:
    BadRequestApiKey:
      description: >-
        Invalid request shape or an unrecognized pointer. Codes:
        `invalid_request`, `invalid_url`.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnauthorizedApiKey:
      description: >-
        Code `unauthenticated` — the API key is missing, malformed, revoked, or
        does not exist. Every operation in this document is authenticated, so
        this is the most common error path a client will hit; it is declared on
        all of them rather than left implicit. The response never distinguishes
        "no such key" from "revoked key". Unlike every other API-key response it
        carries no `X-RateLimit-Limit`: that is a per-account figure, and an
        unauthenticated request has no resolved account to report one for.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PaymentRequiredApiKey:
      description: >-
        Code `payment_required` — available account credits cannot cover the
        quote ceiling.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    QuoteNotFoundApiKey:
      description: >-
        Code `quote_not_found` — no such quote for this account. Deliberately
        one answer for "never existed" and "belongs to another account": the
        quote is keyed under the account that took it, so the read misses either
        way, and a distinguishable refusal would turn a quote id into an oracle
        for whether another tenant holds one.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ConflictApiKeyConfirm:
      description: >-
        The quote can no longer be confirmed as it stands, or the key was
        reused. Codes: `idempotency_conflict` (the same `Idempotency-Key` with a
        different body), `quote_expired` (past the quote's `expires_at`; take a
        new quote), `quote_mismatch` (an enclosure was republished since the
        quote; the message names the episode — take a new quote),
        `expected_total_mismatch` (the `expected_total_credits` sent disagrees
        with the quote's `total_ceiling_credits`; re-read the quote and restate
        its total, or take a new one), and `quote_unverified` (the fence could
        not re-probe every entry inside its budget; retryable — nothing was
        reserved, so send the same confirm again, or take a new quote if it
        keeps happening). None of the others is retryable as sent. The confirm
        spends, so it also answers the account-state three —
        `account_suspended`, `account_closed`, `account_not_found` — which
        refuse before any member is reserved and are about the account rather
        than the quote: a new quote does not help, and the quote itself is
        untouched.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequestsApiKey:
      description: >-
        Codes `rate_limited` or `concurrency_limited`, from two different limits
        and two different places. `rate_limited` is the per-account request
        rate, refused at the edge before the operation runs: it carries
        `Retry-After` and no `X-RateLimit-Limit`, because nothing that knows the
        caller's tier has run yet. `concurrency_limited` is the cap on jobs open
        at once, refused by the operation itself: it carries `X-RateLimit-Limit`
        like every other answered response, and no `Retry-After`, because it
        clears when one of the account's own jobs finishes rather than after a
        fixed wait.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalErrorApiKey:
      description: >-
        Code `internal_error` — a failure on our side that no other code
        describes. The message is fixed text and says nothing about the cause;
        `request_id` is what identifies it in our logs. Retryable.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    apiKeyBearer:
      type: http
      scheme: bearer
      bearerFormat: hk_live_... or hk_test_...
      description: >-
        `Authorization: Bearer hk_live_...` for live keys or `Authorization:
        Bearer hk_test_...` for test-mode keys. `hk_test_` keys resolve real
        public catalog metadata but return deterministic committed fixtures,
        never call inference, and never mutate live credits. This is the *only*
        transport for the credential: the `x-api-key` alias once documented was
        removed in 0.2.0, because the edge authorizer reads `Authorization` as
        its single identity source and a request on any other header is refused
        before it is authenticated.


        Never accepted on a `dashboardJwt` operation, and there are no
        exceptions. `GET /v1/usage` and `GET /v1/limits` briefly declared both
        schemes (0.8.0); that was withdrawn in 0.8.1 because no deployed route
        could honor it — both operations are served by the control-plane API,
        whose authorizer verifies a Cognito token and refuses an `hk_live_`
        credential on shape, and the customer API does not route either path. An
        API-key holder reads its balance and reservation from `QuoteResponse`,
        which carries `balance_credits` and `reserved_credits` on every quote.
        Every operation in this document takes one scheme or the other and
        refuses the wrong one as `unauthenticated`.

````