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

# Approve or decline a connection request

> The signed-in account holder's answer. Approving connects the app to this account: a one-use authorization code and the connection (`grant_id`) are created and the request consumed in one write, and `redirect_to` is the app's redirect carrying `code`, `state`, and `iss`. Declining consumes the request and `redirect_to` carries `error=access_denied`. The caller sends the browser to `redirect_to`; this API never redirects. A request approved twice connects once.



## OpenAPI

````yaml /openapi.yaml post /v1/oauth/requests/{request_id}/decision
openapi: 3.1.0
info:
  title: Audivo API
  version: 0.16.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, by the episode id an earlier answer gave you, or by an upload of
    audio you hold; get back structured, timed text — at once for an episode
    already transcribed, or as a job you poll for fresh audio. One call does it:
    `POST /v1/transcripts`. Quotes are for pricing a selection of many episodes
    before confirming it.


    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.


    The hosted MCP server at `/mcp` and the OAuth 2.1 endpoints that connect an
    assistant to it — `/.well-known/oauth-protected-resource`,
    `/.well-known/oauth-authorization-server`, and `/oauth/authorize`,
    `/oauth/token`, `/oauth/register`, `/oauth/revoke` — are protocol transports
    on the same host, not contract operations: an MCP client discovers and
    describes them from those two metadata documents (RFC 9728, RFC 8414), and
    they are documented with the MCP server. Their access tokens are checked by
    the same authorizer as an API key and metered by the same plan; the consent
    they need is two dashboard operations below, under **Connected Apps**.


    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, 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://api.audivo.dev
    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: Connected Apps
    description: >-
      Dashboard-JWT consent for an assistant connecting to the MCP server
      through OAuth, and the account's connected apps, each revocable.
  - name: Billing
    description: Dashboard-JWT Stripe Checkout, Customer Portal, invoices, and refunds.
  - name: Data
    description: Dashboard-JWT portable export and account deletion.
  - name: Uploads
    description: Audio the caller puts in Audivo's bucket and names as a source (ADR-0031).
paths:
  /v1/oauth/requests/{request_id}/decision:
    post:
      tags:
        - Connected Apps
      summary: Approve or decline a connection request
      description: >-
        The signed-in account holder's answer. Approving connects the app to
        this account: a one-use authorization code and the connection
        (`grant_id`) are created and the request consumed in one write, and
        `redirect_to` is the app's redirect carrying `code`, `state`, and `iss`.
        Declining consumes the request and `redirect_to` carries
        `error=access_denied`. The caller sends the browser to `redirect_to`;
        this API never redirects. A request approved twice connects once.
      operationId: decideOAuthRequest
      parameters:
        - $ref: '#/components/parameters/OAuthRequestIdPathParam'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthDecisionRequest'
      responses:
        '200':
          description: Where to send the browser.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthDecision'
        '400':
          $ref: '#/components/responses/BadRequestDashboard'
        '401':
          $ref: '#/components/responses/UnauthorizedDashboard'
        '404':
          $ref: '#/components/responses/NotFoundDashboard'
        '409':
          $ref: '#/components/responses/ConflictAccountStateDashboard'
        '429':
          $ref: '#/components/responses/TooManyRequestsDashboard'
        '500':
          $ref: '#/components/responses/InternalErrorDashboard'
      security:
        - dashboardJwt: []
components:
  parameters:
    OAuthRequestIdPathParam:
      name: request_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/OAuthRequestId'
  schemas:
    OAuthDecisionRequest:
      type: object
      additionalProperties: false
      required:
        - approve
      properties:
        approve:
          type: boolean
    OAuthDecision:
      type: object
      additionalProperties: false
      required:
        - redirect_to
      properties:
        redirect_to:
          type: string
          format: uri
          maxLength: 4096
          description: >-
            The app's redirect, carrying `code`, `state` and `iss` on an
            approval, or `error=access_denied` on a refusal.
        grant_id:
          $ref: '#/components/schemas/GrantId'
    OAuthRequestId:
      type: string
      pattern: ^oar_[a-f0-9]{24}$
      maxLength: 40
      description: >-
        A pending OAuth authorization request, as the consent page's URL carries
        it.
    GrantId:
      type: string
      pattern: ^oag_[a-f0-9]{24}$
      maxLength: 40
      description: A connected app — one OAuth grant.
    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
                - upload_mismatch
                - max_credits_exceeded
            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
                - upload_not_received
            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
                - upload_not_found
                - oauth_request_not_found
                - connected_app_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
                - upload_quota_exceeded
            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
        - upload_quota_exceeded
        - upload_not_found
        - upload_not_received
        - upload_mismatch
        - max_credits_exceeded
        - oauth_request_not_found
        - connected_app_not_found
      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`,
        `upload_not_found`, `oauth_request_not_found`, `connected_app_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`, `upload_not_received` → 409; `upload_not_received` is
        retryable once the file's PUT has landed. `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`,
        `upload_mismatch`, `max_credits_exceeded` → 422. `content_blocked` →
        451. `rate_limited`, `concurrency_limited`, `upload_quota_exceeded` →
        429; `upload_quota_exceeded` is retryable, at the earliest capacity
        return time its message names. `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
    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:
    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'
    NotFoundDashboard:
      description: >-
        The referenced resource does not exist or does not belong to this
        account. Codes: `api_key_not_found` (`DELETE /v1/api-keys/{key_id}`),
        `credit_lot_not_found` (`POST /v1/billing/refund`),
        `oauth_request_not_found` (the two `/v1/oauth/requests/{request_id}`
        operations: decided already, or past its ten minutes), or
        `connected_app_not_found` (`DELETE /v1/connected-apps/{grant_id}`) —
        each operation emits only the one code for the resource it looks up.
      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'
    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.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.