POST https://api.transcriptmagic.com/api/podcast/transcript
GET https://api.transcriptmagic.com/api/podcast/jobs/{jobId}
Podcast transcription is asynchronous . You submit a Spotify episode URL, receive a jobId, then poll until the transcript is ready. Because podcasts rarely ship captions, the transcript is generated with AI speech-to-text from the episode’s audio (resolved from the show’s RSS feed).
Podcast transcription is available on the Plus and Pro plans. A key on the Free plan or a legacy one-time credit pack receives 402 upgrade_required. Billing is 10 credits per audio-hour (rounded up, with a short grace window), reserved when the job is submitted and refunded automatically if the job fails.
Step 1 — Submit a job
cURL
Python
JavaScript
Go
PHP
curl -X POST https://api.transcriptmagic.com/api/podcast/transcript \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://open.spotify.com/episode/4rOoJ6Egrf8K2IrywzwOMk"}'
import requests
resp = requests.post(
"https://api.transcriptmagic.com/api/podcast/transcript" ,
headers = { "Authorization" : "Bearer YOUR_API_KEY" },
json = { "url" : "https://open.spotify.com/episode/4rOoJ6Egrf8K2IrywzwOMk" },
)
job = resp.json()
print (job[ "jobId" ], job[ "status" ]) # e.g. "abc123" "processing"
const resp = await fetch ( "https://api.transcriptmagic.com/api/podcast/transcript" , {
method: "POST" ,
headers: {
"Authorization" : "Bearer YOUR_API_KEY" ,
"Content-Type" : "application/json" ,
},
body: JSON . stringify ({ url: "https://open.spotify.com/episode/4rOoJ6Egrf8K2IrywzwOMk" }),
});
const job = await resp . json ();
console . log ( job . jobId , job . status ); // "abc123" "processing"
package main
import (
" bytes "
" encoding/json "
" fmt "
" net/http "
)
func main () {
body , _ := json . Marshal ( map [ string ] string {
"url" : "https://open.spotify.com/episode/4rOoJ6Egrf8K2IrywzwOMk" ,
})
req , _ := http . NewRequest ( "POST" ,
"https://api.transcriptmagic.com/api/podcast/transcript" ,
bytes . NewBuffer ( body ))
req . Header . Set ( "Authorization" , "Bearer YOUR_API_KEY" )
req . Header . Set ( "Content-Type" , "application/json" )
resp , _ := http . DefaultClient . Do ( req )
defer resp . Body . Close ()
var job struct {
JobID string `json:"jobId"`
Status string `json:"status"`
}
json . NewDecoder ( resp . Body ). Decode ( & job )
fmt . Println ( job . JobID , job . Status )
}
<? php
$ch = curl_init ( 'https://api.transcriptmagic.com/api/podcast/transcript' );
curl_setopt ( $ch , CURLOPT_POST , true );
curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , true );
curl_setopt ( $ch , CURLOPT_HTTPHEADER , [
'Authorization: Bearer YOUR_API_KEY' ,
'Content-Type: application/json' ,
]);
curl_setopt ( $ch , CURLOPT_POSTFIELDS , json_encode ([
'url' => 'https://open.spotify.com/episode/4rOoJ6Egrf8K2IrywzwOMk' ,
]));
$job = json_decode ( curl_exec ( $ch ), true );
curl_close ( $ch );
echo $job [ 'jobId' ], ' ' , $job [ 'status' ];
A submitted job returns 202 Accepted :
{
"jobId" : "abc123" ,
"status" : "processing"
}
If the same episode was already transcribed (by any user), the API skips the queue and returns 200 immediately with the finished transcript and "deduped": true. Always check status before deciding to poll.
Step 2 — Poll for the transcript
Poll GET /api/podcast/jobs/{jobId} every few seconds until status is done (or failed). Episodes transcribe in well under real time.
cURL
Python
JavaScript
Go
PHP
curl https://api.transcriptmagic.com/api/podcast/jobs/abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
import time, requests
def wait_for_transcript ( job_id , key ):
while True :
r = requests.get(
f "https://api.transcriptmagic.com/api/podcast/jobs/ { job_id } " ,
headers = { "Authorization" : f "Bearer { key } " },
).json()
if r[ "status" ] == "done" :
return r[ "transcript" ]
if r[ "status" ] == "failed" :
raise RuntimeError (r[ "error" ][ "message" ])
time.sleep( 5 )
print (wait_for_transcript( "abc123" , "YOUR_API_KEY" ))
async function waitForTranscript ( jobId , key ) {
while ( true ) {
const r = await fetch (
`https://api.transcriptmagic.com/api/podcast/jobs/ ${ jobId } ` ,
{ headers: { Authorization: `Bearer ${ key } ` } },
). then (( res ) => res . json ());
if ( r . status === "done" ) return r . transcript ;
if ( r . status === "failed" ) throw new Error ( r . error . message );
await new Promise (( res ) => setTimeout ( res , 5000 ));
}
}
console . log ( await waitForTranscript ( "abc123" , "YOUR_API_KEY" ));
package main
import (
" encoding/json "
" fmt "
" net/http "
" time "
)
func main () {
key := "YOUR_API_KEY"
jobID := "abc123"
for {
req , _ := http . NewRequest ( "GET" ,
"https://api.transcriptmagic.com/api/podcast/jobs/" + jobID , nil )
req . Header . Set ( "Authorization" , "Bearer " + key )
resp , _ := http . DefaultClient . Do ( req )
var r struct {
Status string `json:"status"`
Transcript string `json:"transcript"`
Error struct { Message string } `json:"error"`
}
json . NewDecoder ( resp . Body ). Decode ( & r )
resp . Body . Close ()
if r . Status == "done" {
fmt . Println ( r . Transcript )
return
}
if r . Status == "failed" {
fmt . Println ( "failed:" , r . Error . Message )
return
}
time . Sleep ( 5 * time . Second )
}
}
<? php
$key = 'YOUR_API_KEY' ;
$jobId = 'abc123' ;
while ( true ) {
$ch = curl_init ( "https://api.transcriptmagic.com/api/podcast/jobs/ $jobId " );
curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , true );
curl_setopt ( $ch , CURLOPT_HTTPHEADER , [ "Authorization: Bearer $key " ]);
$r = json_decode ( curl_exec ( $ch ), true );
curl_close ( $ch );
if ( $r [ 'status' ] === 'done' ) { echo $r [ 'transcript' ]; break ; }
if ( $r [ 'status' ] === 'failed' ) { echo 'failed: ' . $r [ 'error' ][ 'message' ]; break ; }
sleep ( 5 );
}
While processing
{
"jobId" : "abc123" ,
"status" : "processing" ,
"progress" : { "chunksDone" : 3 , "chunksTotal" : 8 },
"episode" : {
"title" : "The Episode Title" ,
"showTitle" : "The Show" ,
"artworkUrl" : "https://…/artwork.jpg" ,
"durationSeconds" : 4051
},
"createdAt" : "2026-07-05T19:00:00.000Z" ,
"completedAt" : null
}
When done
{
"jobId" : "abc123" ,
"status" : "done" ,
"progress" : { "chunksDone" : 8 , "chunksTotal" : 8 },
"episode" : {
"title" : "The Episode Title" ,
"showTitle" : "The Show" ,
"artworkUrl" : "https://…/artwork.jpg" ,
"durationSeconds" : 4051
},
"transcript" : "Welcome back to the show. Today we're talking about…" ,
"credits" : 2940 ,
"createdAt" : "2026-07-05T19:00:00.000Z" ,
"completedAt" : "2026-07-05T19:00:42.000Z"
}
When failed
{
"jobId" : "abc123" ,
"status" : "failed" ,
"error" : {
"code" : "transcription_failed" ,
"message" : "This episode's audio could not be retrieved"
}
}
credits reflects your remaining balance after the reservation settles; failed jobs are refunded automatically.
Format Example Episode link https://open.spotify.com/episode/4rOoJ6Egrf8K2IrywzwOMk
Paste an individual episode URL. Show, playlist, or artist links don’t identify a single episode and are rejected with 400.
FAQ
How long should I wait between polls?
Every 3–5 seconds is plenty. Most episodes finish in seconds to a couple of minutes depending on length; a multi-hour episode is streamed through a container and still finishes far faster than real time.
How is billing calculated?
10 credits per audio-hour, rounded up, with a short grace window on the first few minutes. A 30-minute episode is 10 credits; a 2-hour episode is 20. Credits are reserved at submit and refunded if the job fails.
Why did I get 402 upgrade_required?
Podcast transcription is a Plus/Pro feature. Keys on the Free plan or a legacy one-time credit pack can still use every social-video endpoint, but not the podcast endpoint. Upgrade at transcriptmagic.com/credits .
Are transcripts timestamped?
No. Podcast transcripts are returned as plain text. episode.durationSeconds gives the total episode length.