Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Audivo API Keys 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.
tags:
- name: API Keys
description: Dashboard-JWT key lifecycle management.
paths:
/v1/api-keys:
get:
operationId: listApiKeys
tags:
- API Keys
summary: List API keys for the account
security:
- dashboardJwt: []
description: Key metadata only; never returns stored plaintext.
responses:
'200':
description: The account's API keys.
headers:
X-Request-Id:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiKeysListResponse'
examples:
keys:
$ref: '#/components/examples/ApiKeysListExample'
'400':
$ref: '#/components/responses/BadRequestDashboard'
'401':
$ref: '#/components/responses/UnauthorizedDashboard'
'429':
$ref: '#/components/responses/TooManyRequestsDashboard'
'500':
$ref: '#/components/responses/InternalErrorDashboard'
post:
operationId: createApiKey
tags:
- API Keys
summary: Create a labeled API key
security:
- dashboardJwt: []
description: Returns the plaintext secret exactly once.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ApiKeyCreateRequest'
examples:
live:
summary: A live key
value:
label: Production server
mode: live
responses:
'201':
description: The created key, including its one-time plaintext secret.
headers:
X-Request-Id:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/ApiKeyCreateResponse'
examples:
created:
$ref: '#/components/examples/ApiKeyCreatedExample'
'400':
$ref: '#/components/responses/BadRequestDashboard'
'401':
$ref: '#/components/responses/UnauthorizedDashboard'
'409':
$ref: '#/components/responses/ConflictApiKeyCreate'
'429':
$ref: '#/components/responses/TooManyRequestsDashboard'
'500':
$ref: '#/components/responses/InternalErrorDashboard'
/v1/api-keys/{key_id}:
delete:
operationId: revokeApiKey
tags:
- API Keys
summary: Revoke an API key
security:
- dashboardJwt: []
description: Revokes immediately; idempotent.
parameters:
- $ref: '#/components/parameters/KeyIdPathParam'
responses:
'204':
description: The key is revoked (or was already revoked).
headers:
X-Request-Id:
$ref: '#/components/headers/XRequestId'
'401':
$ref: '#/components/responses/UnauthorizedDashboard'
'404':
$ref: '#/components/responses/NotFoundDashboard'
'429':
$ref: '#/components/responses/TooManyRequestsDashboard'
'500':
$ref: '#/components/responses/InternalErrorDashboard'
components:
schemas:
ApiKeyPrefix:
type: string
enum:
- hk_live_
- hk_test_
Error:
type: object
additionalProperties: false
required:
- error
properties:
error:
$ref: '#/components/schemas/ErrorDetail'
ApiKeyCreateRequest:
type: object
additionalProperties: false
required:
- label
properties:
label:
type: string
minLength: 1
maxLength: 100
pattern: ^[^\r\n]{1,100}$
mode:
$ref: '#/components/schemas/ApiKeyMode'
default: live
KeyId:
type: string
pattern: ^key_[A-Za-z0-9]{16,32}$
maxLength: 40
description: Opaque, server-generated API key identifier.
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.
ApiKeyCreateResponse:
type: object
additionalProperties: false
required:
- key_id
- label
- prefix
- mode
- created_at
- revoked
- secret
description: Same fields as `ApiKeySummary` plus the plaintext `secret`, shown exactly once. Duplicated rather than composed via `allOf` for the same reason documented on `TranscriptArtifact`.
properties:
key_id:
$ref: '#/components/schemas/KeyId'
label:
type: string
minLength: 1
maxLength: 100
prefix:
$ref: '#/components/schemas/ApiKeyPrefix'
mode:
$ref: '#/components/schemas/ApiKeyMode'
created_at:
type: string
format: date-time
revoked:
type: boolean
secret:
type: string
minLength: 20
maxLength: 200
description: The plaintext secret. Shown exactly once; never retrievable again.
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.'
ApiKeyMode:
type: string
enum:
- live
- test
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
ApiKeysListResponse:
type: object
additionalProperties: false
required:
- data
properties:
data:
type: array
maxItems: 200
items:
$ref: '#/components/schemas/ApiKeySummary'
RequestId:
type: string
pattern: ^req_[A-Za-z0-9-]+$
maxLength: 100
ApiKeySummary:
type: object
additionalProperties: false
required:
- key_id
- label
- prefix
- mode
- created_at
- revoked
description: Key metadata only; never returns stored plaintext.
properties:
key_id:
$ref: '#/components/schemas/KeyId'
label:
type: string
minLength: 1
maxLength: 100
prefix:
$ref: '#/components/schemas/ApiKeyPrefix'
mode:
$ref: '#/components/schemas/ApiKeyMode'
created_at:
type: string
format: date-time
last_used_at:
$ref: '#/components/schemas/NullableDateTime'
revoked:
type: boolean
revoked_at:
$ref: '#/components/schemas/NullableDateTime'
NullableDateTime:
description: An ISO 8601 timestamp, or `null`.
anyOf:
- type: string
format: date-time
- enum:
- null
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'
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'
BadRequestDashboard:
description: 'Code: `invalid_request`.'
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'
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'
ConflictApiKeyCreate:
description: 'The account cannot be issued a key right now. Codes: `api_key_limit_reached` (this account already holds the maximum number of live keys — revoke one you no longer use), or one of the account-state three, `account_suspended` / `account_closed` / `account_not_found`. `POST /v1/api-keys` is the first dashboard operation that writes, which is why it is the first to declare them. Revocation deliberately does not: switching off a credential stays available on an account that has been suspended, since a hold is when it matters most.'
headers:
X-Request-Id:
$ref: '#/components/headers/XRequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
parameters:
KeyIdPathParam:
name: key_id
in: path
required: true
schema:
$ref: '#/components/schemas/KeyId'
examples:
ApiKeyCreatedExample:
summary: A newly created key, secret shown once
value:
key_id: key_3xzM1LpN9k2fA7bQ
label: Production server
prefix: hk_live_
mode: live
created_at: '2026-08-28T09:00:00Z'
revoked: false
secret: hk_live_3f8a9c2b1d4e9f2c1d4e9f2c1d4e9f2c
ApiKeysListExample:
summary: The account's API keys
value:
data:
- key_id: key_3xzM1LpN9k2fA7bQ
label: Production server
prefix: hk_live_
mode: live
created_at: '2026-07-01T00:00:00Z'
last_used_at: '2026-08-27T12:00:00Z'
revoked: false
revoked_at: null
- key_id: key_7mZaK4tRbQ2vXpL9
label: CI test fixtures
prefix: hk_test_
mode: test
created_at: '2026-07-01T00:00:00Z'
last_used_at: null
revoked: true
revoked_at: '2026-08-01T00: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
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
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`.'
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.'