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

# Price a selection without spending

> Turns a chart or a list of named shows into a priced selection and records it. **Reserves nothing**: the quote holds no credit and creates no job; a separate confirm operation, keyed on `quote_id`, is what spends.

**The selection rule, stated once.** Each show contributes either the episodes named in its `episode_ids`, or — when none are named — its newest `episodes_per_show` (default 1). Two tier bounds apply: the chart size, and the total number of episodes one selection may come to. No show may give more than that total on its own, which is the only thing that bounds depth: how far back a selection reaches is the caller's to state, not the plan's to grant. Every bound that bit is reported in `clamps`; every show and every episode that did not make it into `entries` is reported in `excluded` with a reason a caller can read. Nothing shrinks silently.

**Two request shapes.** `chart` asks the provider for a category chart (clamped exactly as `GET /v1/charts` clamps it) and, because nobody chose those shows, excludes music-led ones unless `include_music_led` is `true`. `shows` names feed URLs — usually straight from a search result — and takes every one as chosen; no category data is consulted for a named show.

**One feed fetch per show.** The feeds read to select episodes are the ones the quote prices from; nothing is downloaded twice. Each phase runs under its own wall-clock budget inside the request, and an episode the budget did not reach comes back in `excluded` as `budget_exceeded` rather than failing the request.

Every priced entry carries the domain's estimate and its ceiling; a cached entry is priced at what a cached read costs *this* account — the floor for the account whose job produced the transcript, a fraction of its measured audio minutes for anyone else — with no ceiling above it. `remaining_open_jobs` is the caller's fan-out headroom under their tier's open-job cap — jobs reserved and not yet terminal — reported so a selection can be sized before spending. It is enforced at confirm, not here. `balance_credits` and `reserved_credits` ride beside it off the same account row, so an agent sees what it holds at the moment it decides to spend; they are reported on the same terms, and a balance too small for `total_ceiling_credits` does not refuse the quote.



## OpenAPI

````yaml /openapi.yaml post /v1/quotes
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:
    post:
      tags:
        - Quotes
      summary: Price a selection without spending
      description: >-
        Turns a chart or a list of named shows into a priced selection and
        records it. **Reserves nothing**: the quote holds no credit and creates
        no job; a separate confirm operation, keyed on `quote_id`, is what
        spends.


        **The selection rule, stated once.** Each show contributes either the
        episodes named in its `episode_ids`, or — when none are named — its
        newest `episodes_per_show` (default 1). Two tier bounds apply: the chart
        size, and the total number of episodes one selection may come to. No
        show may give more than that total on its own, which is the only thing
        that bounds depth: how far back a selection reaches is the caller's to
        state, not the plan's to grant. Every bound that bit is reported in
        `clamps`; every show and every episode that did not make it into
        `entries` is reported in `excluded` with a reason a caller can read.
        Nothing shrinks silently.


        **Two request shapes.** `chart` asks the provider for a category chart
        (clamped exactly as `GET /v1/charts` clamps it) and, because nobody
        chose those shows, excludes music-led ones unless `include_music_led` is
        `true`. `shows` names feed URLs — usually straight from a search result
        — and takes every one as chosen; no category data is consulted for a
        named show.


        **One feed fetch per show.** The feeds read to select episodes are the
        ones the quote prices from; nothing is downloaded twice. Each phase runs
        under its own wall-clock budget inside the request, and an episode the
        budget did not reach comes back in `excluded` as `budget_exceeded`
        rather than failing the request.


        Every priced entry carries the domain's estimate and its ceiling; a
        cached entry is priced at what a cached read costs *this* account — the
        floor for the account whose job produced the transcript, a fraction of
        its measured audio minutes for anyone else — with no ceiling above it.
        `remaining_open_jobs` is the caller's fan-out headroom under their
        tier's open-job cap — jobs reserved and not yet terminal — reported so a
        selection can be sized before spending. It is enforced at confirm, not
        here. `balance_credits` and `reserved_credits` ride beside it off the
        same account row, so an agent sees what it holds at the moment it
        decides to spend; they are reported on the same terms, and a balance too
        small for `total_ceiling_credits` does not refuse the quote.
      operationId: createQuote
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteRequest'
            examples:
              fromChart:
                $ref: '#/components/examples/QuoteFromChartRequest'
              fromShows:
                $ref: '#/components/examples/QuoteFromShowsRequest'
      responses:
        '200':
          description: >-
            The recorded quote — priced entries, exclusions, clamps, totals, and
            headroom.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuoteResponse'
              examples:
                quote:
                  $ref: '#/components/examples/QuoteExample'
        '400':
          $ref: '#/components/responses/BadRequestApiKey'
        '401':
          $ref: '#/components/responses/UnauthorizedApiKey'
        '422':
          $ref: '#/components/responses/NothingToQuoteApiKey'
        '429':
          $ref: '#/components/responses/TooManyRequestsApiKey'
        '500':
          $ref: '#/components/responses/InternalErrorApiKey'
        '503':
          $ref: '#/components/responses/DiscoveryUnavailableApiKey'
      security:
        - apiKeyBearer: []
components:
  schemas:
    QuoteRequest:
      oneOf:
        - $ref: '#/components/schemas/QuoteFromShows'
        - $ref: '#/components/schemas/QuoteFromChart'
    QuoteResponse:
      type: object
      additionalProperties: false
      required:
        - quote_id
        - source
        - episodes_per_show
        - entries
        - excluded
        - clamps
        - cached_members
        - uncached_members
        - total_ceiling_credits
        - remaining_open_jobs
        - balance_credits
        - reserved_credits
        - expires_at
        - created_at
      description: >-
        The recorded quote. `total_ceiling_credits` is the sum of the entries'
        ceilings and `cached_members` the count of entries with `is_cached:
        true` — both derived by the store at write time, never taken from a
        caller. The quote is valid until `expires_at`; a confirm after that is
        refused.


        `balance_credits` and `reserved_credits` are the caller's account as it
        stood when the quote was priced, read off the same row
        `remaining_open_jobs` was counted from — so an agent can see what it
        holds at the moment it decides to spend without a second call. Like the
        headroom beside them they are **reported, never enforced**: a quote
        reserves nothing, so a balance below `total_ceiling_credits` is a fact
        to act on rather than a refusal, and affordability is decided by the
        confirm.
      properties:
        quote_id:
          $ref: '#/components/schemas/QuoteId'
        source:
          type: string
          enum:
            - shows
            - chart
          description: Which request shape produced this quote.
        episodes_per_show:
          type: integer
          minimum: 1
          description: The N after the tier had its say.
        entries:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/QuoteEntry'
        excluded:
          type: array
          items:
            $ref: '#/components/schemas/QuoteExclusion'
        clamps:
          type: array
          maxItems: 3
          items:
            $ref: '#/components/schemas/SelectionClamp'
        cached_members:
          type: integer
          minimum: 0
        uncached_members:
          type: integer
          minimum: 0
        total_ceiling_credits:
          $ref: '#/components/schemas/CreditAmount'
        remaining_open_jobs:
          type: integer
          minimum: 0
          description: >-
            How many more jobs the caller's tier lets them have open — reserved
            and not yet terminal — right now, so a selection can be sized before
            it is confirmed. Reported here, enforced at confirm.
        balance_credits:
          $ref: '#/components/schemas/CreditAmount'
        reserved_credits:
          $ref: '#/components/schemas/CreditAmount'
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    QuoteFromShows:
      type: object
      additionalProperties: false
      required:
        - shows
      properties:
        shows:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/NamedShow'
        episodes_per_show:
          type: integer
          minimum: 1
          maximum: 100
          default: 1
          description: >-
            The N in "the newest N episodes per show". Ignored for a show whose
            `episode_ids` name its episodes. Clamped to the plan's whole
            selection total when it exceeds it, and reported.
        include_music_led:
          type: boolean
          description: >-
            The music-led switch. Named shows are taken as chosen, so it changes
            nothing here; it is accepted so one client can send one body shape
            for both sources.
    QuoteFromChart:
      type: object
      additionalProperties: false
      required:
        - chart
      properties:
        chart:
          $ref: '#/components/schemas/ChartSelection'
        episodes_per_show:
          type: integer
          minimum: 1
          maximum: 100
          default: 1
          description: >-
            The N in "the newest N episodes per show". Ignored for a show whose
            `episode_ids` name its episodes. Clamped to the plan's whole
            selection total when it exceeds it, and reported.
        include_music_led:
          type: boolean
          default: false
          description: >-
            The music-led opt-in. A chart is uncurated, so music-led entries are
            excluded from the selection — each named in `excluded` — unless this
            is `true`.
    QuoteId:
      type: string
      pattern: ^qte_[A-Za-z0-9]{16,32}$
      maxLength: 40
      description: Opaque, server-generated quote identifier.
    QuoteEntry:
      type: object
      additionalProperties: false
      required:
        - episode_id
        - show_id
        - feed_url
        - guid
        - show_title
        - episode_title
        - published_at
        - is_cached
        - estimated_credits
        - quote_ceiling_credits
        - quote_basis
        - declared_duration_seconds
      description: >-
        One priced episode. An uncached entry carries the domain's estimate from
        the feed-declared duration and its ceiling; a cached one is priced at
        what a cached read costs the calling account with no ceiling above it —
        there is no measurement left to exceed.
      properties:
        episode_id:
          $ref: '#/components/schemas/EpisodeId'
        show_id:
          $ref: '#/components/schemas/ShowId'
        feed_url:
          $ref: '#/components/schemas/FeedUrl'
        guid:
          $ref: '#/components/schemas/EpisodeGuid'
        show_title:
          type: string
          minLength: 1
          maxLength: 300
        episode_title:
          anyOf:
            - type: string
              maxLength: 300
            - enum:
                - null
        published_at:
          $ref: '#/components/schemas/NullableDateTime'
        is_cached:
          type: boolean
        estimated_credits:
          $ref: '#/components/schemas/CreditAmount'
        quote_ceiling_credits:
          $ref: '#/components/schemas/CreditAmount'
        quote_basis:
          $ref: '#/components/schemas/QuoteBasis'
        declared_duration_seconds:
          type: number
          minimum: 0
          description: >-
            The feed's `<itunes:duration>`, in seconds — what the estimate is
            priced from.
    QuoteExclusion:
      type: object
      additionalProperties: false
      required:
        - feed_url
        - guid
        - title
        - reason
        - detail
      description: >-
        One thing the caller asked for that is not in `entries`. `guid` is
        `null` when a whole show was excluded; `feed_url` is `null` only when
        the provider carried none. `title` is what a surface shows a human — the
        show's name, or `show — episode` for one episode.
      properties:
        feed_url:
          anyOf:
            - $ref: '#/components/schemas/FeedUrl'
            - enum:
                - null
        guid:
          anyOf:
            - $ref: '#/components/schemas/EpisodeGuid'
            - enum:
                - null
        title:
          type: string
          minLength: 1
          maxLength: 600
        reason:
          $ref: '#/components/schemas/QuoteExclusionReason'
        detail:
          type: string
          minLength: 1
          maxLength: 1000
    SelectionClamp:
      type: object
      additionalProperties: false
      required:
        - dimension
        - requested
        - allowed
        - limit
        - clamped
        - detail
      description: A tier ceiling that bit, in the terms the caller used.
      properties:
        dimension:
          type: string
          enum:
            - chart_size
            - episodes_per_show
            - total_selection
        requested:
          type: integer
          minimum: 1
        allowed:
          type: integer
          minimum: 0
        limit:
          type: integer
          minimum: 1
        clamped:
          const: true
        detail:
          type: string
          minLength: 1
          maxLength: 1000
    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.
    Error:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorDetail'
    NamedShow:
      type: object
      additionalProperties: false
      required:
        - feed_url
      description: >-
        A show the caller chooses by feed URL — usually copied from a
        `ShowSummary`. `itunes_id` travels with it because it changes how
        `show_id` derives; `title` is only the label the quote shows back, and
        defaults to the feed URL.


        `episode_ids` is how a caller reaches an episode that is not among the
        newest: name it, and this show contributes exactly the episodes named
        instead of its newest N. Without it the selection grammar is only "the
        newest N per show", so a back-catalogue episode would be unreachable at
        any tier: `episodes_per_show` sets how wide the newest-N window is,
        never where it sits.
      properties:
        feed_url:
          $ref: '#/components/schemas/FeedUrl'
        episode_ids:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/EpisodeId'
          description: >-
            The exact episodes this show should contribute, as `GET
            /v1/shows/{show_id}/episodes` returns their ids. Omit it to take the
            newest `episodes_per_show` instead. The two are alternatives and do
            not combine: `episodes_per_show` is the N in "the newest N", which a
            caller who named their episodes has said nothing about, so it is not
            applied to them. What still bounds a named selection is the plan's
            total for the whole selection; no tier carries a per-show depth cap.
            An id that matches nothing in the feed comes back as an
            `episode_not_in_feed` exclusion rather than shortening the list
            silently.
        itunes_id:
          anyOf:
            - type: integer
              minimum: 1
            - enum:
                - null
        title:
          type: string
          minLength: 1
          maxLength: 300
    ChartSelection:
      type: object
      additionalProperties: false
      required:
        - category
      description: >-
        The same chart `GET /v1/charts` returns, fetched and clamped inside the
        quote.
      properties:
        category:
          type: string
          minLength: 1
          maxLength: 100
        size:
          type: integer
          minimum: 1
          maximum: 100
          default: 10
          description: Clamped to the tier's chart cap, and the clamp is reported.
        language:
          $ref: '#/components/schemas/Language'
    EpisodeId:
      type: string
      pattern: ^ep_[a-z2-7]{16}$
      description: Canonical episode identifier.
    ShowId:
      type: string
      pattern: ^sh_[a-z2-7]{16}$
      description: Canonical show identifier.
    FeedUrl:
      type: string
      format: uri
      minLength: 1
      maxLength: 2048
      description: An RSS/Atom feed URL.
    EpisodeGuid:
      type: string
      minLength: 1
      maxLength: 512
      description: >-
        An episode GUID as published in the feed's `<guid>` element. Used
        verbatim — GUIDs are case- and whitespace-significant.
    NullableDateTime:
      description: An ISO 8601 timestamp, or `null`.
      anyOf:
        - type: string
          format: date-time
        - enum:
            - null
    QuoteBasis:
      type: string
      enum:
        - feed_metadata
        - probed
      description: >-
        `feed_metadata` when the estimate is derived from feed-published
        duration; `probed` when a bounded enclosure probe measured it.
    QuoteExclusionReason:
      type: string
      enum:
        - feed_unavailable
        - feed_unparseable
        - budget_exceeded
        - music_led
        - no_feed_url
        - duplicate_show
        - chart_size_capped
        - total_selection_capped
        - no_episodes
        - no_guid
        - no_enclosure
        - episode_not_in_feed
        - episode_not_found
        - no_declared_duration
        - no_stable_asset_revision
      description: >-
        Every reason a show or an episode can be missing from a quote, in one
        closed vocabulary shared by the selection rule and the quote: the caller
        reads one list, not two. The `nothing_to_quote` error message counts
        these same words.
    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
    Language:
      type: string
      pattern: ^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$
      maxLength: 35
      description: >-
        A BCP-47 language tag (general syntax check, not full IANA subtag
        validation).
    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
  examples:
    QuoteFromChartRequest:
      summary: Quote the newest 2 episodes of each show in a 20-show News chart
      value:
        chart:
          category: News
          size: 20
          language: en
        episodes_per_show: 2
    QuoteFromShowsRequest:
      summary: Quote the newest episode of two shows picked from a search
      value:
        shows:
          - feed_url: https://feeds.example.com/vergecast.xml
            itunes_id: 430333725
            title: The Vergecast
          - feed_url: https://feeds.example.com/decoder.xml
            itunes_id: null
        episodes_per_show: 2
    QuoteExample:
      summary: >-
        A Hobby-key quote — one cached entry, two uncached, one show past its
        depth
      value:
        quote_id: qte_9k2fA7bQ3xzM1LpN
        source: shows
        episodes_per_show: 2
        entries:
          - episode_id: ep_rwg4do2imnjyhaj7
            show_id: sh_k4n7qzvw2mtxyabc
            feed_url: https://feeds.example.com/vergecast.xml
            guid: vergecast-2026-09-05
            show_title: The Vergecast
            episode_title: The week in gadgets
            published_at: '2026-09-05T09:00:00Z'
            is_cached: true
            estimated_credits: 1
            quote_ceiling_credits: 1
            quote_basis: feed_metadata
            declared_duration_seconds: 3600
          - episode_id: ep_3iicyxg6ymbv75gy
            show_id: sh_k4n7qzvw2mtxyabc
            feed_url: https://feeds.example.com/vergecast.xml
            guid: vergecast-2026-09-02
            show_title: The Vergecast
            episode_title: Foldables, again
            published_at: '2026-09-02T09:00:00Z'
            is_cached: false
            estimated_credits: 60
            quote_ceiling_credits: 75
            quote_basis: feed_metadata
            declared_duration_seconds: 3600
          - episode_id: ep_uy4pqvjsmeyrhavz
            show_id: sh_x5z7grcs3p4y5h62
            feed_url: https://feeds.example.com/decoder.xml
            guid: decoder-2026-09-04
            show_title: https://feeds.example.com/decoder.xml
            episode_title: null
            published_at: '2026-09-04T09:00:00Z'
            is_cached: false
            estimated_credits: 45
            quote_ceiling_credits: 57
            quote_basis: feed_metadata
            declared_duration_seconds: 2700
        excluded:
          - feed_url: https://feeds.example.com/decoder.xml
            guid: decoder-2026-08-28
            title: https://feeds.example.com/decoder.xml — Decoder, 28 August
            reason: no_declared_duration
            detail: >-
              the feed declares no <itunes:duration>, so there is nothing to
              price from
        clamps: []
        cached_members: 1
        uncached_members: 2
        total_ceiling_credits: 133
        remaining_open_jobs: 2
        balance_credits: 2840
        reserved_credits: 0
        expires_at: '2026-09-08T09:15:00Z'
        created_at: '2026-09-08T09:00:00Z'
  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
  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'
    NothingToQuoteApiKey:
      description: >-
        Code `nothing_to_quote` — the request was well-formed and every show or
        episode in it was excluded from the selection. The message counts the
        reasons (`music_led ×10`, `feed_unavailable ×2`, …) using the same
        vocabulary as `QuoteResponse.excluded`, so a caller can act on it: opt
        in to music-led shows, pick a different chart, or retry a feed that was
        down. Nothing is recorded for a quote that priced nothing.
      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'
    DiscoveryUnavailableApiKey:
      description: >-
        Code `discovery_unavailable` — the show directory (Podcast Index) could
        not be reached, refused the request, or answered something this API
        cannot read as a show list. Retryable; a directory outage is never
        answered with an empty list, because an empty list is a valid answer to
        "shows in this category" and a caller would believe it.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
      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`.

````