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

# The account's recent API requests and what each one cost

> One entry per request the account made: when it arrived, the endpoint it called, how it ended, and the credits it was charged. Newest first, paged with `limit` and `cursor`.

**`credits_charged` is what was actually spent, never what was held.** A request that submits work answers `202` having reserved a ceiling and spent nothing, and its charge is written later, when the job settles — so the figure here starts at `0` and grows as the work lands. It never falls: a reservation that is released is never charged. `charge_pending` is `true` for exactly those requests, so a zero that is about to change is distinguishable from a request that was free. Every figure is the same ledger `GET /v1/usage` reports from, so the two pages cannot disagree about one request.

**`omitted` says what this log cannot show.** Requests refused at the edge — an unrecognized or revoked API key, and the per-minute rate limit — never reach the API, and the component that refuses them authenticates with a single read and writes nothing, so no record of them exists. Their absence is stated rather than left to be discovered.

Entries are retained for 30 days and then expire.



## OpenAPI

````yaml /openapi.yaml get /v1/logs
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/logs:
    get:
      tags:
        - Account
      summary: The account's recent API requests and what each one cost
      description: >-
        One entry per request the account made: when it arrived, the endpoint it
        called, how it ended, and the credits it was charged. Newest first,
        paged with `limit` and `cursor`.


        **`credits_charged` is what was actually spent, never what was held.** A
        request that submits work answers `202` having reserved a ceiling and
        spent nothing, and its charge is written later, when the job settles —
        so the figure here starts at `0` and grows as the work lands. It never
        falls: a reservation that is released is never charged. `charge_pending`
        is `true` for exactly those requests, so a zero that is about to change
        is distinguishable from a request that was free. Every figure is the
        same ledger `GET /v1/usage` reports from, so the two pages cannot
        disagree about one request.


        **`omitted` says what this log cannot show.** Requests refused at the
        edge — an unrecognized or revoked API key, and the per-minute rate limit
        — never reach the API, and the component that refuses them authenticates
        with a single read and writes nothing, so no record of them exists.
        Their absence is stated rather than left to be discovered.


        Entries are retained for 30 days and then expire.
      operationId: listRequestLogs
      parameters:
        - $ref: '#/components/parameters/LimitQueryParam'
        - $ref: '#/components/parameters/CursorQueryParam'
      responses:
        '200':
          description: One page of the account's recent requests, newest first.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RequestLogListResponse'
              examples:
                logs:
                  $ref: '#/components/examples/RequestLogListExample'
        '400':
          $ref: '#/components/responses/BadRequestDashboard'
        '401':
          $ref: '#/components/responses/UnauthorizedDashboard'
        '429':
          $ref: '#/components/responses/TooManyRequestsDashboard'
        '500':
          $ref: '#/components/responses/InternalErrorDashboard'
      security:
        - dashboardJwt: []
components:
  parameters:
    LimitQueryParam:
      name: limit
      in: query
      required: false
      description: Maximum number of items to return.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    CursorQueryParam:
      name: cursor
      in: query
      required: false
      description: Opaque pagination cursor from a previous page's `next_cursor`.
      schema:
        $ref: '#/components/schemas/Cursor'
  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
    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
  schemas:
    RequestLogListResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - next_cursor
        - omitted
      properties:
        data:
          type: array
          maxItems: 100
          description: Requests newest first.
          items:
            $ref: '#/components/schemas/RequestLogEntry'
        next_cursor:
          $ref: '#/components/schemas/NullableCursor'
        omitted:
          $ref: '#/components/schemas/RequestLogOmission'
    Cursor:
      type: string
      pattern: ^[A-Za-z0-9_-]{1,512}$
      description: An opaque pagination cursor. Callers must treat it as opaque.
    RequestLogEntry:
      type: object
      additionalProperties: false
      required:
        - request_id
        - at
        - method
        - endpoint
        - status
        - duration_ms
        - credits_charged
        - charge_pending
        - error_code
      properties:
        request_id:
          $ref: '#/components/schemas/RequestId'
        at:
          type: string
          format: date-time
          description: When the API finished handling the request.
        method:
          type: string
          enum:
            - GET
            - POST
            - PUT
            - PATCH
            - DELETE
        endpoint:
          type: string
          maxLength: 200
          description: >-
            The route template the request matched, e.g.
            `/v1/episodes/{episode_id}/transcript` — not the path as called. A
            log entry names a route, never its contents: no identifier a caller
            passed, no query string, and no request body is recorded.
          example: /v1/episodes/{episode_id}/transcript
        status:
          type: integer
          minimum: 100
          maximum: 599
        duration_ms:
          type: integer
          minimum: 0
          description: How long the API took to answer, in milliseconds.
        credits_charged:
          $ref: '#/components/schemas/CreditAmount'
          description: >-
            Credits actually charged for this request so far. `0` for the
            requests that cost nothing, and for a submission whose work has not
            settled yet — see `charge_pending`.
        charge_pending:
          type: boolean
          description: >-
            `true` when this request started work that is charged as it lands,
            so `credits_charged` may still grow. `false` when the figure is
            final. A boolean rather than a credit amount, which is why it is not
            named `credits_*`.
        error_code:
          description: >-
            The error code the request was refused with, or `null` if it
            succeeded.
          anyOf:
            - $ref: '#/components/schemas/ErrorCode'
            - enum:
                - null
    NullableCursor:
      description: An opaque pagination cursor, or `null` when there is no next page.
      anyOf:
        - $ref: '#/components/schemas/Cursor'
        - enum:
            - null
    RequestLogOmission:
      type: object
      additionalProperties: false
      required:
        - reason
        - message
      description: >-
        What this log does not contain, stated in every response including an
        empty one.
      properties:
        reason:
          type: string
          enum:
            - refused_at_the_edge
        message:
          type: string
          maxLength: 600
    Error:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorDetail'
    RequestId:
      type: string
      pattern: ^req_[A-Za-z0-9-]+$
      maxLength: 100
    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.
    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.
    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
    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.
  examples:
    RequestLogListExample:
      summary: A charged read, a submission still settling, and a refusal
      value:
        data:
          - request_id: req_7f3a9c2b1d4e4f5a8b6c0d1e2f3a4b5c
            at: '2026-09-11T10:04:12Z'
            method: GET
            endpoint: /v1/episodes/{episode_id}/transcript
            status: 200
            duration_ms: 137
            credits_charged: 1
            charge_pending: false
            error_code: null
          - request_id: req_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
            at: '2026-09-11T10:02:55Z'
            method: POST
            endpoint: /v1/quotes/{quote_id}/confirm
            status: 200
            duration_ms: 1840
            credits_charged: 22
            charge_pending: true
            error_code: null
          - request_id: req_9f8e7d6c5b4a39281706f5e4d3c2b1a0
            at: '2026-09-11T09:58:01Z'
            method: POST
            endpoint: /v1/transcripts
            status: 402
            duration_ms: 88
            credits_charged: 0
            charge_pending: false
            error_code: payment_required
        next_cursor: eyJzayI6IkxPRyMyMDI2
        omitted:
          reason: refused_at_the_edge
          message: >-
            Requests refused before they reached the API are not listed here: a
            key that was not recognized or has been revoked, and requests turned
            away by the per-minute rate limit. Those are refused at the edge,
            which authenticates without writing anything, so there is no record
            of them to show. Everything your key was allowed to run appears
            above.
  responses:
    BadRequestDashboard:
      description: 'Code: `invalid_request`.'
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    UnauthorizedDashboard:
      description: >-
        Code `unauthenticated` — the Cognito JWT is missing, malformed, expired,
        or was rejected. An API key presented to one of the operations that
        declare this response lands here too: they take a session and nothing
        else, and that is an authentication failure, not an authorization one.
        That includes `GET /v1/usage` and `GET /v1/limits`, which are served by
        the control-plane API alone.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    TooManyRequestsDashboard:
      description: Code `rate_limited`, e.g. plan-change rate limiting.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalErrorDashboard:
      description: >-
        Code `internal_error` — a call on our side failed and nothing was
        changed. On `POST /v1/session` this is what an unreachable identity
        service or an unreadable configuration answers with, deliberately rather
        than `401`: the session is not the problem, and telling a signed-in
        customer their token is bad during an outage of ours sends them into a
        sign-in loop. Retryable; `request_id` is what identifies it in our logs.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    dashboardJwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        A Cognito access token, issued through the dashboard's own sign-up and
        sign-in pages (a backend-for-frontend; there is no Cognito Hosted UI)
        and sent as `Authorization: Bearer <jwt>`. Dashboard operations require
        this scheme and cannot be called with an API key, with no exception.
        `GET /v1/usage` and `GET /v1/limits` offered both schemes from 0.8.0;
        0.8.1 withdrew that, because the control-plane API serves them alone and
        its authorizer has no API-key branch. An `account_id` supplied by the
        client is ignored; all reads/writes are scoped server-side to the
        authenticated account.

````