> ## Documentation Index
> Fetch the complete documentation index at: https://docs.transcriptmagic.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Apple Podcasts

> Transcribe an Apple Podcasts episode with an async submit + poll flow.

```http theme={null}
POST https://api.transcriptmagic.com/api/podcast/transcript
GET  https://api.transcriptmagic.com/api/podcast/jobs/{jobId}
```

Podcast transcription is **asynchronous**. You submit an Apple Podcasts 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).

<Note>
  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.
</Note>

## Step 1 — Submit a job

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.transcriptmagic.com/api/podcast/transcript \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"url":"https://podcasts.apple.com/us/podcast/my-first-million/id1469759170?i=1000662680822"}'
  ```

  ```python Python theme={null}
  import requests

  resp = requests.post(
      "https://api.transcriptmagic.com/api/podcast/transcript",
      headers={"Authorization": "Bearer YOUR_API_KEY"},
      json={"url": "https://podcasts.apple.com/us/podcast/my-first-million/id1469759170?i=1000662680822"},
  )
  job = resp.json()
  print(job["jobId"], job["status"])  # e.g. "abc123" "processing"
  ```

  ```javascript JavaScript theme={null}
  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://podcasts.apple.com/us/podcast/my-first-million/id1469759170?i=1000662680822",
    }),
  });
  const job = await resp.json();
  console.log(job.jobId, job.status); // "abc123" "processing"
  ```

  ```go Go theme={null}
  package main

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"net/http"
  )

  func main() {
  	body, _ := json.Marshal(map[string]string{
  		"url": "https://podcasts.apple.com/us/podcast/my-first-million/id1469759170?i=1000662680822",
  	})
  	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 PHP theme={null}
  <?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://podcasts.apple.com/us/podcast/my-first-million/id1469759170?i=1000662680822',
  ]));
  $job = json_decode(curl_exec($ch), true);
  curl_close($ch);
  echo $job['jobId'], ' ', $job['status'];
  ```
</CodeGroup>

A submitted job returns **`202 Accepted`**:

```json theme={null}
{
  "jobId": "abc123",
  "status": "processing"
}
```

<Note>
  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.
</Note>

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

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.transcriptmagic.com/api/podcast/jobs/abc123 \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```python Python theme={null}
  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"))
  ```

  ```javascript JavaScript theme={null}
  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"));
  ```

  ```go Go theme={null}
  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 PHP theme={null}
  <?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);
  }
  ```
</CodeGroup>

### While processing

```json theme={null}
{
  "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

```json theme={null}
{
  "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

```json theme={null}
{
  "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

| Format       | Example                                                                 |
| ------------ | ----------------------------------------------------------------------- |
| Episode link | `https://podcasts.apple.com/us/podcast/<slug>/id<showId>?i=<episodeId>` |

Paste an individual **episode** URL — it must include the `?i=<episodeId>` query parameter. A show page without `?i=` points at the whole show, not a single episode, and is rejected with `400`.

## FAQ

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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](https://transcriptmagic.com/credits).
  </Accordion>

  <Accordion title="Are transcripts timestamped?">
    No. Podcast transcripts are returned as plain text. `episode.durationSeconds` gives the total episode length.
  </Accordion>
</AccordionGroup>
