curl --request POST \
--url https://k8mfogcvz4.execute-api.us-east-1.amazonaws.com/prod/v1/transcripts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "https://podcasts.apple.com/us/podcast/x/id123?i=456",
"language": "en",
"format": "json",
"dry_run": false
}
'{
"format": "json",
"is_cached": true,
"credits_charged": 4503599627370495,
"transcript": {
"episode_id": "<string>",
"show_id": "<string>",
"language": "<string>",
"duration_sec": 64800,
"source": "qwen3-asr",
"source_revision": "<string>",
"model_version": "<string>",
"pipeline_version": "<string>",
"timing_precision": "word",
"diarized": false,
"warnings": [
{
"segment": 500000,
"type": "<string>",
"detail": "<string>"
}
],
"segments": [
{
"id": 500000,
"start": 64800,
"end": 64800,
"speaker": null,
"text": "<string>",
"words": [
{
"w": "<string>",
"s": 64800,
"e": 64800
}
]
}
],
"created_at": "2023-11-07T05:31:56Z"
},
"transcript_url": "<string>",
"expires_at": "2023-11-07T05:31:56Z"
}Create or attach to a transcription job, or get an instant result
Accepts exactly one of three input shapes (url, feed_url + guid, or episode_id); supplying more than one, or fields from more than one shape, fails schema validation. Returns a synchronous result on a cache hit or publisher-transcript passthrough, or 202 with a job to poll on a miss. dry_run: true returns a quote and creates neither a job nor a reservation.
Every job belongs to a group. A single-episode submission is a job group of one: the same submission sequence a quote’s confirm fans out over N members, run once, so the two can never disagree. The accepted job carries its group_id, which GET /v1/groups/{group_id} and POST /v1/groups/{group_id}/cancel accept. An Idempotency-Key, when sent, is honoured at the group: a repeat returns the original job — or the original cached read — and charges nothing further; its status is then the job’s current state. A repeat that arrives while the original is still running — its group begun, its member not yet landed — is 409 request_in_progress: retry with the same key shortly, never a new one, which would start a second job for the same episode.
Not yet served on this path (0.4.0). format other than json is refused with invalid_request here — the read operations
deliver the raw formats — and a cache hit above the inline limit is delivered by reference as TranscriptRead.transcript_url; language is accepted and recorded nowhere.
Delivery format. format: json (or omitted) returns application/json. format: text|srt|vtt|md returns the raw artifact under its own media type (text/plain, application/x-subrip, text/vtt, text/markdown) — never a JSON wrapper around a string. Because a raw body has nowhere to put provenance, those responses carry it in headers instead: X-Transcript-Episode-Id, X-Transcript-Source, X-Transcript-Timing-Precision, and X-Credits-Charged. The single content-type switch: a payload over the ~5 MB inline limit returns 200 application/json with { transcript_url, expires_at } in every format — including the raw ones, whose declared media type therefore does not apply to that one case. A 202, a dry-run quote, and every error are always application/json regardless of format.
curl --request POST \
--url https://k8mfogcvz4.execute-api.us-east-1.amazonaws.com/prod/v1/transcripts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "https://podcasts.apple.com/us/podcast/x/id123?i=456",
"language": "en",
"format": "json",
"dry_run": false
}
'{
"format": "json",
"is_cached": true,
"credits_charged": 4503599627370495,
"transcript": {
"episode_id": "<string>",
"show_id": "<string>",
"language": "<string>",
"duration_sec": 64800,
"source": "qwen3-asr",
"source_revision": "<string>",
"model_version": "<string>",
"pipeline_version": "<string>",
"timing_precision": "word",
"diarized": false,
"warnings": [
{
"segment": 500000,
"type": "<string>",
"detail": "<string>"
}
],
"segments": [
{
"id": 500000,
"start": 64800,
"end": 64800,
"speaker": null,
"text": "<string>",
"words": [
{
"w": "<string>",
"s": 64800,
"e": 64800
}
]
}
],
"created_at": "2023-11-07T05:31:56Z"
},
"transcript_url": "<string>",
"expires_at": "2023-11-07T05:31:56Z"
}Authorizations
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.
Headers
Account-scoped for 24 hours; stores a canonical request-body hash. Reuse with a different body returns 409 idempotency_conflict; a repeat while the original request is still running returns 409 request_in_progress (retry with the same key). This is separate from the cache lease used to coalesce inference.
1 - 255^[!-~]{1,255}$Body
- Option 1
- Option 2
- Option 3
Exactly one of url, feed_url+guid, or episode_id. format is delivery-only and never affects the cache key or the produced transcript. Raw-audio URLs, uploads, engine selection, diarization, vocabulary, and callback URLs are rejected rather than ignored.
An Apple Podcasts episode URL.
1 - 2048A BCP-47 language tag, or null/omitted to auto-detect. Automatic detection never selects the ASR engine: it always routes to the configured default engine, and detected language is recorded only as a reporting field. Reaching a language outside the default engine's coverage requires passing this field explicitly.
35^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$Delivery-only: never affects the cache key or the produced transcript content. Every format is derived at read time from the one canonical transcript. Response media type per format: json → application/json, text → text/plain, srt → application/x-subrip, vtt → text/vtt, md → text/markdown. The one exception is an oversized payload, which is application/json in every format (see TranscriptUrlRef).
json, text, srt, vtt, md Response
A cache hit or publisher-transcript passthrough (charges credits_charged), or a dry-run quote (dry_run: true; creates no job or reservation). The media type follows format; see the operation description for the one documented switch back to application/json.
- Option 1
- Option 2
- Option 3
- Option 4
A synchronous JSON transcript read: a cache hit or publisher passthrough (POST /v1/transcripts), a direct cached fetch (GET /v1/episodes/{episode_id}/transcript), or the re-delivery of a read that was already paid for (GET /v1/reads/{read_id}). credits_charged is what this request charged, which is why it is 0 on the last of the three and the cached-read price for this account on the other two. format is always json here — a text|srt|vtt|md read of the same transcript is delivered as a raw body with X-Credits-Charged carrying this envelope's credits_charged. See the comment above for why this shape is self-contained rather than composed via allOf.
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.
0 <= x <= 9007199254740991Show child attributes
Show child attributes
2048
