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

# Account credit balance and period usage

> The account's balance, what it has spent in the current reporting period, and that spend day by day.

**The period is not a Stripe subscription cycle yet, because no subscription exists.** It is the whole month that began on the account's own creation anniversary — the same shape a subscription cycle has, so the figures do not change shape when billing lands, and per-account rather than a calendar month, so an account created on the 28th is not shown three days of history and told it is a month.

`allowance_resets_at` is the date the plan's included credits refill, and it is `null` on the free tier: that grant is issued once at signup and nothing refills it, so a date there would be a promise nothing keeps. `additional_credits` is what the account holds *beyond* what the plan included — a promotional grant, or a purchase — read from the credit lots, which are the only record of where a credit came from.

`daily` runs from the period's first day through today (or through the period's last day, once it is over), zero-filled, and always sums to `period_used_credits`. A day counts a credit when it was **settled**: a reservation is not spend, and credits released by a cancelled job were never used.

Refused with `account_closed` on an account that is being deleted or is deleted. That is the one account state this read refuses: deletion retains the credit records so a refund or a dispute can still be answered, and this is the operation that reads them. A *suspended* account is answered normally — a hold is a reason not to spend, not a reason to stop showing a customer what they have spent.



## OpenAPI

````yaml /openapi.yaml get /v1/usage
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/usage:
    get:
      tags:
        - Account
      summary: Account credit balance and period usage
      description: >-
        The account's balance, what it has spent in the current reporting
        period, and that spend day by day.


        **The period is not a Stripe subscription cycle yet, because no
        subscription exists.** It is the whole month that began on the account's
        own creation anniversary — the same shape a subscription cycle has, so
        the figures do not change shape when billing lands, and per-account
        rather than a calendar month, so an account created on the 28th is not
        shown three days of history and told it is a month.


        `allowance_resets_at` is the date the plan's included credits refill,
        and it is `null` on the free tier: that grant is issued once at signup
        and nothing refills it, so a date there would be a promise nothing
        keeps. `additional_credits` is what the account holds *beyond* what the
        plan included — a promotional grant, or a purchase — read from the
        credit lots, which are the only record of where a credit came from.


        `daily` runs from the period's first day through today (or through the
        period's last day, once it is over), zero-filled, and always sums to
        `period_used_credits`. A day counts a credit when it was **settled**: a
        reservation is not spend, and credits released by a cancelled job were
        never used.


        Refused with `account_closed` on an account that is being deleted or is
        deleted. That is the one account state this read refuses: deletion
        retains the credit records so a refund or a dispute can still be
        answered, and this is the operation that reads them. A *suspended*
        account is answered normally — a hold is a reason not to spend, not a
        reason to stop showing a customer what they have spent.
      operationId: getUsage
      responses:
        '200':
          description: Balance snapshot plus period usage and reservations.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-RateLimit-Limit:
              $ref: '#/components/headers/XRateLimitLimit'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
              examples:
                usage:
                  $ref: '#/components/examples/UsageExample'
        '401':
          $ref: '#/components/responses/UnauthorizedDashboard'
        '409':
          $ref: '#/components/responses/ConflictAccountStateDashboard'
        '429':
          $ref: '#/components/responses/TooManyRequestsDashboard'
      security:
        - dashboardJwt: []
components:
  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
  schemas:
    UsageResponse:
      type: object
      additionalProperties: false
      required:
        - tier
        - balance_credits
        - reserved_credits
        - included_credits
        - additional_credits
        - period_start
        - period_end
        - period_used_credits
        - allowance_resets_at
        - daily
      description: >-
        What the account holds and what it has spent this period. See `GET
        /v1/usage` for what the period is while no subscription exists, and why
        `allowance_resets_at` is `null` on the free tier.
      properties:
        tier:
          $ref: '#/components/schemas/Tier'
        balance_credits:
          $ref: '#/components/schemas/CreditAmount'
        reserved_credits:
          $ref: '#/components/schemas/CreditAmount'
        included_credits:
          $ref: '#/components/schemas/CreditAmount'
        additional_credits:
          $ref: '#/components/schemas/CreditAmount'
        period_start:
          type: string
          format: date-time
        period_end:
          type: string
          format: date-time
        period_used_credits:
          $ref: '#/components/schemas/CreditAmount'
        allowance_resets_at:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            When the plan's included credits next refill. `null` when nothing
            refills them — the free tier's grant is issued once at signup, and
            there is no sweep that renews it.
        daily:
          type: array
          maxItems: 31
          description: >-
            The period's spend by day, from its first day through today,
            zero-filled and always summing to `period_used_credits`. It stops at
            today rather than running to `period_end`: a zero for a day that has
            not happened is not a fact.
          items:
            $ref: '#/components/schemas/DailyUsage'
    Tier:
      type: string
      enum:
        - free
        - hobby
        - startup
        - growth
        - scale
    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.
    DailyUsage:
      type: object
      additionalProperties: false
      required:
        - date
        - used_credits
      description: >-
        One UTC calendar day of the reporting period, and what was settled in
        it.
      properties:
        date:
          type: string
          format: date
          description: >-
            A UTC calendar day. UTC and not the viewer's zone: every timestamp
            this system stores is UTC, and a local-zone series would move
            credits between days depending on who is looking at it.
        used_credits:
          $ref: '#/components/schemas/CreditAmount'
    Error:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          $ref: '#/components/schemas/ErrorDetail'
    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.
    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:
    UsageExample:
      summary: Balance snapshot, period usage, and the period so far by day
      value:
        tier: startup
        balance_credits: 4820
        reserved_credits: 335
        included_credits: 3000
        additional_credits: 2000
        period_start: '2026-08-14T09:00:00Z'
        period_end: '2026-09-14T09:00:00Z'
        period_used_credits: 1180
        allowance_resets_at: '2026-09-14T09:00:00Z'
        daily:
          - date: '2026-08-14'
            used_credits: 0
          - date: '2026-08-15'
            used_credits: 940
          - date: '2026-08-16'
            used_credits: 240
  responses:
    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'
    ConflictAccountStateDashboard:
      description: >-
        The account's own state refuses this write. Codes: `account_suspended`,
        `account_closed`, or `account_not_found`. The same three
        `ConflictApiKeyCreate` declares, without the key-specific one — a
        separate component so no operation tells its callers about a refusal it
        cannot produce.
      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'
  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.

````