Skip to main content
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 -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 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.

Supported URL formats

FormatExample
Episode linkhttps://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

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.
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.
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.
No. Podcast transcripts are returned as plain text. episode.durationSeconds gives the total episode length.