# Apple Podcasts
Source: https://docs.transcriptmagic.com/api-reference/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).
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
```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}
'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'];
```
A submitted job returns **`202 Accepted`**:
```json theme={null}
{
"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.
```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}
### 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//id?i=` |
Paste an individual **episode** URL — it must include the `?i=` query parameter. A show page without `?i=` points at the whole show, not a single episode, and is 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](https://transcriptmagic.com/credits).
No. Podcast transcripts are returned as plain text. `episode.durationSeconds` gives the total episode length.
# Get credit balance
Source: https://docs.transcriptmagic.com/api-reference/balance
Read your current credit balance and plan. Does not consume a credit.
```http theme={null}
GET https://api.transcriptmagic.com/api/balance
```
A read-only check that returns your current credit balance and plan. **No credit is charged.** Use it to validate a freshly-issued key, surface remaining credits in your UI, or trigger a top-up flow before a batch job. Rate limit still applies — 120 requests per minute per key.
## Request
```bash cURL theme={null}
curl https://api.transcriptmagic.com/api/balance \
-H "Authorization: Bearer YOUR_API_KEY"
```
```python Python theme={null}
import requests
response = requests.get(
"https://api.transcriptmagic.com/api/balance",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
data = response.json()
print(f"Credits remaining: {data['credits']} (plan: {data['plan']})")
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.transcriptmagic.com/api/balance", {
headers: { "Authorization": "Bearer YOUR_API_KEY" },
});
const data = await response.json();
console.log(`Credits remaining: ${data.credits} (plan: ${data.plan})`);
```
```go Go theme={null}
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET",
"https://api.transcriptmagic.com/api/balance", nil)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var data struct {
Credits int `json:"credits"`
Plan string `json:"plan"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Printf("Credits remaining: %d (plan: %s)\n", data.Credits, data.Plan)
}
```
```php PHP theme={null}
## Response
```json theme={null}
{
"credits": 487,
"plan": "credits"
}
```
| Field | Type | Description |
| --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `credits` | integer | Remaining credits on the account. `0` means out of credits — transcript endpoints will return `403 no_credits` until you top up. |
| `plan` | string | One of `"free"`, `"credits"`, or a subscription plan slug (e.g. `"plus"`, `"pro"`). |
### Response headers
Every response carries the standard rate-limit headers:
| Header | Description |
| ----------------------- | ---------------------------------------------- |
| `X-RateLimit-Limit` | Always `120`. |
| `X-RateLimit-Remaining` | Requests left in the current 60-second window. |
| `X-RateLimit-Reset` | Unix timestamp when the window resets. |
## Errors
| Status | Body | When |
| ------ | ------------------------------------------------------ | ---------------------------------------------------------------------- |
| `401` | `{ "error": "Missing API key" }` | No `Authorization` header, or it doesn't start with `Bearer sk_live_`. |
| `401` | `{ "error": "Invalid API key" }` | The key was revoked or never existed. |
| `429` | `{ "error": "Rate limit exceeded. Try again later." }` | 120 requests/minute exceeded. Honor the `Retry-After` header. |
Unlike the transcript endpoints, `/api/balance` does **not** return `403 no_credits` for empty accounts — it is the canonical way to check whether you should top up before issuing a transcript request.
## Common patterns
Call `/api/balance` once at the start of a batch of 1,000 URLs. If `credits < 1000`, fail fast rather than burning through credits mid-run and getting partial results.
Use `/api/balance` as your smoke test in deploy pipelines — a `200` confirms the key is valid and points at a real account. Cheaper and faster than POSTing a real video URL.
Cache the response for \~30 seconds in your app shell. Refresh after every successful transcript call (which already returns `credits` in its response body) so users see the deduction immediately.
# Facebook
Source: https://docs.transcriptmagic.com/api-reference/facebook
Fetch transcripts from Facebook Reels, video posts, and Live replays.
```http theme={null}
POST https://api.transcriptmagic.com/api/facebook/transcript
```
Reels, video posts, Watch URLs, posts containing video, and `fb.watch` short links are all supported.
## Request
```bash cURL theme={null}
curl -X POST https://api.transcriptmagic.com/api/facebook/transcript \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.facebook.com/user/videos/123456789/"}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.transcriptmagic.com/api/facebook/transcript",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"url": "https://www.facebook.com/user/videos/123456789/"},
)
data = response.json()
print(data["transcript"]) # plain string, newline-separated
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.transcriptmagic.com/api/facebook/transcript", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://www.facebook.com/user/videos/123456789/" }),
});
const data = await response.json();
console.log(data.transcript); // plain string, newline-separated
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"url": "https://www.facebook.com/user/videos/123456789/",
})
req, _ := http.NewRequest("POST",
"https://api.transcriptmagic.com/api/facebook/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 data struct {
Transcript string `json:"transcript"`
VideoUrls *struct {
Sd string `json:"sd"`
Hd string `json:"hd"`
Thumbnail string `json:"thumbnail"`
} `json:"videoUrls,omitempty"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data.Transcript)
}
```
```php PHP theme={null}
'https://www.facebook.com/user/videos/123456789/',
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo $data['transcript'];
```
## Response
```json theme={null}
{
"transcript": "Hey everyone, today I want to walk\nyou through how we built the new product line...",
"videoUrls": {
"sd": "https://...mp4",
"hd": "https://...mp4",
"thumbnail": "https://...jpg"
},
"credits": 994
}
```
`transcript` is a plain string with newlines between spoken lines — there is no per-line timing in the Facebook response and no `title` field. `videoUrls` is included when the upstream exposes a downloadable video URL (Facebook usually provides both `sd` and `hd`); it is omitted when neither is available.
## Supported URL formats
| Format | Example |
| ---------------------- | ---------------------------------------------------------------- |
| Standard video post | `https://www.facebook.com/{user}/videos/{id}/` |
| Reels | `https://www.facebook.com/reel/` |
| Watch | `https://www.facebook.com/watch/?v=` |
| Posts containing video | `https://www.facebook.com/{user}/posts/` |
| `fb.watch` short link | `https://fb.watch//` |
| Live replay | Completed Live videos served at the standard `/videos//` URL |
The bare `facebook.com/watch` Watch home page (no `?v=`) is rejected with `400 Invalid URL` — the validator requires a `v` query parameter.
## FAQ
No. Only publicly viewable videos can be transcribed.
Group-only videos aren't supported because the API can't authenticate as a group member.
No — only completed Live replays. Live-in-progress streams don't have stable transcripts to fetch.
If a video URL is available but no captions, you'll get a 200 with `transcript: ""` and a `videoUrls` object — and you'll be charged 1 credit. If the upstream has neither captions nor a video URL, you'll get `404 No transcript available for this video` and you won't be charged.
# Instagram
Source: https://docs.transcriptmagic.com/api-reference/instagram
Fetch transcripts from Instagram Reels, video posts, and IGTV with one REST call.
```http theme={null}
POST https://api.transcriptmagic.com/api/instagram/transcript
```
Reels (`/reel/…`), video posts (`/p/…`), and IGTV (`/tv/…`) URLs are all supported. Stories are not (they expire and don't have stable URLs).
## Request
```bash cURL theme={null}
curl -X POST https://api.transcriptmagic.com/api/instagram/transcript \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.instagram.com/reel/ABC123/"}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.transcriptmagic.com/api/instagram/transcript",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"url": "https://www.instagram.com/reel/ABC123/"},
)
data = response.json()
# Instagram returns a `transcripts` array (plural) of {text} objects
for seg in data.get("transcripts", []):
print(seg["text"])
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.transcriptmagic.com/api/instagram/transcript", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://www.instagram.com/reel/ABC123/" }),
});
const data = await response.json();
// Instagram returns a `transcripts` array (plural) of {text} objects
for (const seg of (data.transcripts || [])) {
console.log(seg.text);
}
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"url": "https://www.instagram.com/reel/ABC123/",
})
req, _ := http.NewRequest("POST",
"https://api.transcriptmagic.com/api/instagram/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 data struct {
Transcripts []struct {
Text string `json:"text"`
} `json:"transcripts"`
}
json.NewDecoder(resp.Body).Decode(&data)
for _, seg := range data.Transcripts {
fmt.Println(seg.Text)
}
}
```
```php PHP theme={null}
'https://www.instagram.com/reel/ABC123/',
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
foreach (($data['transcripts'] ?? []) as $seg) {
echo $seg['text'] . "\n";
}
```
## Response
```json theme={null}
{
"success": true,
"transcripts": [
{
"id": "3870781634534573841",
"shortcode": "DW3x2NpigcR",
"text": "Five things I wish I knew before I started this challenge..."
}
],
"credits": 995
}
```
Instagram returns its transcript as a `transcripts` array (note the **plural**). Each entry has `id`, `shortcode`, and `text`. The `text` field holds the full transcript as a single string — there is no per-line timing on this endpoint, no `transcript` (singular) field, and no `title`. Reels typically return a single entry; the array shape is preserved for potential future multi-segment responses.
## Supported URL formats
| Format | Example |
| ---------- | --------------------------------------------- |
| Reel | `https://www.instagram.com/reel//` |
| Video post | `https://www.instagram.com/p//` |
| IGTV | `https://www.instagram.com/tv//` |
Stories are not supported because they expire and don't have stable, crawlable URLs.
## FAQ
No. Private accounts and stories behind a follow-gate aren't supported. Only publicly viewable Reels and posts can be transcribed.
Stories aren't supported because they expire and don't have stable, crawlable URLs. Reels and video posts are the supported formats.
POST the URL of the specific video item in the carousel (Instagram exposes per-item URLs). The API transcribes the single video at that URL.
Instagram's upstream caption source emits plain text lines without timing metadata. If you need per-second timing, use the YouTube endpoint instead — it's the only platform on this API that returns `startMs`/`endMs` per segment.
# LinkedIn
Source: https://docs.transcriptmagic.com/api-reference/linkedin
Fetch transcripts from public LinkedIn posts containing video.
```http theme={null}
POST https://api.transcriptmagic.com/api/linkedin/transcript
```
Public LinkedIn posts that contain a video are supported. The transcript is returned as plain text with no per-line timing.
## Request
```bash cURL theme={null}
curl -X POST https://api.transcriptmagic.com/api/linkedin/transcript \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.linkedin.com/posts/artificial-analysis_some-slug-activity-7465082408409870337-4Pm-"}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.transcriptmagic.com/api/linkedin/transcript",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"url": "https://www.linkedin.com/posts/artificial-analysis_some-slug-activity-7465082408409870337-4Pm-"},
)
data = response.json()
print(data["transcript"]) # plain string, newline-separated
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.transcriptmagic.com/api/linkedin/transcript", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://www.linkedin.com/posts/artificial-analysis_some-slug-activity-7465082408409870337-4Pm-" }),
});
const data = await response.json();
console.log(data.transcript); // plain string, newline-separated
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"url": "https://www.linkedin.com/posts/artificial-analysis_some-slug-activity-7465082408409870337-4Pm-",
})
req, _ := http.NewRequest("POST",
"https://api.transcriptmagic.com/api/linkedin/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 data struct {
Transcript string `json:"transcript"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data.Transcript)
}
```
```php PHP theme={null}
'https://www.linkedin.com/posts/artificial-analysis_some-slug-activity-7465082408409870337-4Pm-',
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo $data['transcript'];
```
## Response
```json theme={null}
{
"transcript": "Hey everyone, today I want to walk\nyou through how we built the new product line...",
"credits": 994
}
```
`transcript` is a plain string with newlines between spoken lines — there is no per-line timing in the LinkedIn response and no `videoUrls` field. LinkedIn is a transcript-only platform.
## Supported URL formats
| Format | Example |
| ---------------------- | --------------------------------------------------------------------- |
| Public post with video | `https://www.linkedin.com/posts/{author}_{slug}-activity-{id}-{code}` |
Only public post URLs that contain a video are accepted. Member profile, company page, and article URLs without an embedded video are rejected with `400 Invalid URL`.
## FAQ
No. Only publicly viewable posts can be transcribed. Posts visible to connections only, or to a restricted audience, can't be fetched because the API can't authenticate as a connection.
Only posts that contain a video. Text-only posts, image posts, and document/carousel posts have nothing to transcribe and are rejected.
No. LinkedIn transcripts are returned as plain text with newlines between lines — there is no per-line timing.
If the post's video has no available transcript, you'll get `404 No transcript available` and you won't be charged.
# API reference
Source: https://docs.transcriptmagic.com/api-reference/overview
Seven platforms. Same auth. Per-platform response shapes.
Every endpoint is a `POST` to `https://api.transcriptmagic.com` with `{"url": "..."}` in the body. Auth, path style, and the body field are identical across platforms — but the **response shape differs by platform**, because each upstream source emits different metadata. The per-endpoint pages below document the exact shape you can expect from each.
## Base URL
```
https://api.transcriptmagic.com
```
## Endpoints
| Platform | Path | Guide |
| -------------- | -------------------------------- | ------------------------------------------------- |
| YouTube | `POST /api/youtube/transcript` | [YouTube →](/api-reference/youtube) |
| TikTok | `POST /api/tiktok/transcript` | [TikTok →](/api-reference/tiktok) |
| Instagram | `POST /api/instagram/transcript` | [Instagram →](/api-reference/instagram) |
| Facebook | `POST /api/facebook/transcript` | [Facebook →](/api-reference/facebook) |
| LinkedIn | `POST /api/linkedin/transcript` | [LinkedIn →](/api-reference/linkedin) |
| Rumble | `POST /api/rumble/transcript` | [Rumble →](/api-reference/rumble) |
| X | `POST /api/twitter/transcript` | [X →](/api-reference/twitter) |
| Spotify | `POST /api/podcast/transcript` | [Spotify →](/api-reference/spotify) |
| Apple Podcasts | `POST /api/podcast/transcript` | [Apple Podcasts →](/api-reference/apple-podcasts) |
The social-video endpoints above are synchronous — one request returns the transcript. **Podcast** transcription (Spotify, Apple Podcasts) is asynchronous: submit an episode, then poll `GET /api/podcast/jobs/{jobId}`. It's a Plus/Pro feature billed at 10 credits per audio-hour. See the [Spotify](/api-reference/spotify) and [Apple Podcasts](/api-reference/apple-podcasts) guides.
## Request
All endpoints accept the same body and headers.
**Headers**
```http theme={null}
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
```
**Body**
```json theme={null}
{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
```
## Response
All endpoints return `200 OK` on success and always include a `credits` field with your remaining balance. Beyond that, the shape varies:
| Platform | `transcript` type | Per-line timing | Video URLs included |
| --------- | ------------------------------------------------------- | ----------------------------------------- | ------------------------------------ |
| YouTube | array of segment objects | yes (`startMs`, `endMs`, `startTimeText`) | no |
| TikTok | string (newline-joined) | no | yes (when available, in `videoUrls`) |
| Instagram | array of `{text}` objects (under `transcripts`, plural) | no | no |
| Facebook | string (newline-joined) | no | yes (when available, in `videoUrls`) |
| LinkedIn | string (newline-joined) | no | no |
| Rumble | string (newline-joined) | no | no |
| X | string (newline-joined) | no | no |
See the [response schema](/api-reference/response-schema) page for the full per-platform field reference, or jump into a per-endpoint page for a real example payload.
## Errors
All endpoints share the same error vocabulary — see [errors](/concepts/errors) for the full table. The most common ones:
* `400` — bad URL in the body, missing `url`, or invalid JSON
* `401` — missing or invalid API key
* `403` — out of credits (`error: "no_credits"`)
* `404` — upstream says the video is missing, private, or has no captions
* `429` — over the rate limit
* `502` — upstream platform HTTP error (retry with backoff)
# Response schema
Source: https://docs.transcriptmagic.com/api-reference/response-schema
Per-platform response shapes — what's guaranteed, what varies.
A successful request (`200 OK`) always includes a `credits` field with your remaining balance, but the **rest of the response shape varies by platform** because each upstream source emits different metadata. This page documents the actual shape returned by each endpoint, plus the universal fields and helpers for building SRT/VTT.
## Universal fields
These fields are present on every successful response, regardless of platform:
Your remaining credit balance **after** this call's deduction. On cache hits no credit is deducted, but the field still reflects your current balance.
`true` only when the response was served from the shared transcript cache (in which case no credit was charged). Omitted on fresh fetches.
## YouTube
```json theme={null}
{
"success": true,
"type": "video",
"url": "https://www.youtube.com/watch?v=...",
"transcript": [
{ "text": "...", "startMs": "320", "endMs": "14580", "startTimeText": "0:00" }
],
"transcript_only_text": "... ...",
"language": "English",
"videoId": "...",
"captionTracks": [/* available tracks */],
"credits": 997
}
```
An array of timed segment objects. Each item has `text`, `startMs`, `endMs`, and `startTimeText`. There is no separate `segments` field — these objects ARE the transcript.
The same content as `transcript` flattened to a single string, useful when you don't need timing.
Human-readable name of the caption track that was returned (e.g. `"English"`).
YouTube's 11-character video ID.
Every caption track YouTube exposes for this video — useful if you want to fetch a different language. Each entry has at minimum `baseUrl`, `name.simpleText`, `languageCode`, and `isTranslatable`.
### YouTube segment fields
Spoken text for this segment.
Start time in milliseconds, encoded as a string. Cast to integer for math.
End time in milliseconds, encoded as a string.
Pre-formatted human-readable timestamp, e.g. `"1:23"`.
## TikTok
```json theme={null}
{
"transcript": "First line of dialog\nSecond line of dialog\n...",
"videoUrls": { "sd": "https://...mp4", "hd": null, "thumbnail": "https://...jpg" },
"credits": 996
}
```
The transcript as a single string with newlines separating spoken lines. No per-line timing.
Optional. Present when the upstream exposes a downloadable video. TikTok rarely provides a separate HD URL, so `hd` is typically `null`.
## Facebook
```json theme={null}
{
"transcript": "First line of dialog\nSecond line of dialog\n...",
"videoUrls": { "sd": "https://...mp4", "hd": "https://...mp4", "thumbnail": "https://...jpg" },
"credits": 994
}
```
Joined transcript string, newline-separated.
Optional. Facebook usually provides both `sd` and `hd` URLs.
## Instagram
```json theme={null}
{
"success": true,
"transcripts": [
{
"id": "3870781634534573841",
"shortcode": "DW3x2NpigcR",
"text": "Full transcript as a single string..."
}
],
"credits": 995
}
```
Note the **plural** name. Each entry has `id`, `shortcode`, and `text`. Reels typically return a single entry containing the full transcript text.
Instagram's internal numeric media ID for the post.
The shortcode from the URL (the same one you submitted in the URL path, e.g. `DW3x2NpigcR`).
The full transcript content for this entry, as a single string — no per-line timing.
## LinkedIn, Rumble, and X
These three are transcript-only platforms. Each returns a plain `transcript` string with newlines between spoken lines — no per-line timing and no `videoUrls` field. (Rumble's upstream captions arrive as WEBVTT and are cleaned to plain text server-side before they're returned.)
```json theme={null}
{
"transcript": "First line of dialog\nSecond line of dialog\n...",
"credits": 994
}
```
The transcript as a single string with newlines separating spoken lines. No per-line timing, and no `videoUrls` field on these platforms.
X (Twitter) video tweets must be **under 2 minutes** and are AI-transcribed on demand, so that endpoint is slower than the caption-based platforms.
## Building SRT/VTT (YouTube only)
Only the YouTube endpoint emits per-line timing, so SRT/VTT generation is a YouTube-specific operation:
```javascript theme={null}
function toSrt(transcript) {
return transcript.map((s, i) => {
const start = msToSrtTime(parseInt(s.startMs, 10));
const end = msToSrtTime(parseInt(s.endMs, 10));
return `${i + 1}\n${start} --> ${end}\n${s.text}\n`;
}).join("\n");
}
function msToSrtTime(ms) {
const hours = Math.floor(ms / 3600000);
const minutes = Math.floor((ms % 3600000) / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const millis = ms % 1000;
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)},${pad(millis, 3)}`;
}
function pad(n, len = 2) {
return String(n).padStart(len, "0");
}
// Usage:
// const srt = toSrt(response.transcript);
```
## Why are timestamps strings?
`startMs` and `endMs` on YouTube segments are returned as strings because some upstream platforms emit microsecond-precision values that don't fit cleanly in a 32-bit integer. Strings preserve the value losslessly across all clients. Parse to integer in your application code.
# Rumble
Source: https://docs.transcriptmagic.com/api-reference/rumble
Fetch transcripts from public Rumble videos that have captions.
```http theme={null}
POST https://api.transcriptmagic.com/api/rumble/transcript
```
Public Rumble video URLs are supported when the video has captions. The upstream WEBVTT captions are cleaned to plain text server-side before they're returned.
## Request
```bash cURL theme={null}
curl -X POST https://api.transcriptmagic.com/api/rumble/transcript \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://rumble.com/v79xhhm-some-title.html"}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.transcriptmagic.com/api/rumble/transcript",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"url": "https://rumble.com/v79xhhm-some-title.html"},
)
data = response.json()
print(data["transcript"]) # plain string, newline-separated
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.transcriptmagic.com/api/rumble/transcript", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://rumble.com/v79xhhm-some-title.html" }),
});
const data = await response.json();
console.log(data.transcript); // plain string, newline-separated
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"url": "https://rumble.com/v79xhhm-some-title.html",
})
req, _ := http.NewRequest("POST",
"https://api.transcriptmagic.com/api/rumble/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 data struct {
Transcript string `json:"transcript"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data.Transcript)
}
```
```php PHP theme={null}
'https://rumble.com/v79xhhm-some-title.html',
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo $data['transcript'];
```
## Response
```json theme={null}
{
"transcript": "Hey everyone, today I want to walk\nyou through how we built the new product line...",
"credits": 994
}
```
`transcript` is a plain string with newlines between spoken lines. Rumble's upstream captions arrive as WEBVTT and are cleaned to plain text server-side — timing cues are stripped, so there is no per-line timing and no `videoUrls` field.
## Supported URL formats
| Format | Example |
| ------------------- | -------------------------------------------- |
| Standard video page | `https://rumble.com/v79xhhm-some-title.html` |
The video ID is the `v…` token at the start of the path slug. Embed and channel URLs without a `v…` video slug are rejected with `400 Invalid URL`.
## FAQ
Only videos that have captions. Rumble doesn't auto-caption every video, so coverage depends on whether the uploader (or Rumble) provided captions.
You'll get `404 No transcript available` and you won't be charged.
The upstream captions are WEBVTT, but we strip the timing cues and clean the cue text into a plain newline-separated string server-side. There is no per-line timing in the response.
No. Only publicly viewable videos can be transcribed.
# Spotify Podcasts
Source: https://docs.transcriptmagic.com/api-reference/spotify
Transcribe a Spotify podcast 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 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
```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://open.spotify.com/episode/4rOoJ6Egrf8K2IrywzwOMk"}'
```
```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://open.spotify.com/episode/4rOoJ6Egrf8K2IrywzwOMk"},
)
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://open.spotify.com/episode/4rOoJ6Egrf8K2IrywzwOMk" }),
});
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://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 PHP theme={null}
'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`**:
```json theme={null}
{
"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.
```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}
### 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://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](https://transcriptmagic.com/credits).
No. Podcast transcripts are returned as plain text. `episode.durationSeconds` gives the total episode length.
# TikTok
Source: https://docs.transcriptmagic.com/api-reference/tiktok
Fetch transcripts from any public TikTok video with one REST call.
```http theme={null}
POST https://api.transcriptmagic.com/api/tiktok/transcript
```
Full share URLs (`/@user/video/`) and `vm.tiktok.com` short links both resolve cleanly.
## Request
```bash cURL theme={null}
curl -X POST https://api.transcriptmagic.com/api/tiktok/transcript \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.tiktok.com/@user/video/7212345678901234567"}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.transcriptmagic.com/api/tiktok/transcript",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"url": "https://www.tiktok.com/@user/video/7212345678901234567"},
)
data = response.json()
print(data["transcript"]) # plain string, newline-separated lines
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.transcriptmagic.com/api/tiktok/transcript", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://www.tiktok.com/@user/video/7212345678901234567" }),
});
const data = await response.json();
console.log(data.transcript); // plain string, newline-separated
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"url": "https://www.tiktok.com/@user/video/7212345678901234567",
})
req, _ := http.NewRequest("POST",
"https://api.transcriptmagic.com/api/tiktok/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 data struct {
Transcript string `json:"transcript"`
VideoUrls *struct {
Sd string `json:"sd"`
Hd string `json:"hd"`
Thumbnail string `json:"thumbnail"`
} `json:"videoUrls,omitempty"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data.Transcript)
}
```
```php PHP theme={null}
'https://www.tiktok.com/@user/video/7212345678901234567',
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo $data['transcript'];
```
## Response
```json theme={null}
{
"transcript": "Okay so today I'm going to show you\nthe easiest pasta recipe\nyou'll ever make...",
"videoUrls": {
"sd": "https://...mp4",
"hd": null,
"thumbnail": "https://...jpg"
},
"credits": 996
}
```
`transcript` is a plain string with newlines between spoken lines — there is no per-line timing in the TikTok response, and no `title` field. `videoUrls` is included whenever the upstream exposes a downloadable video URL; it is omitted when none is available.
## Supported URL formats
| Format | Example |
| -------------------------- | -------------------------------------------------- |
| Full share URL | `https://www.tiktok.com/@user/video/<19-digit id>` |
| `vm.tiktok.com` short link | `https://vm.tiktok.com/` |
## FAQ
No. The API can only fetch transcripts for publicly viewable videos. Private accounts and videos behind a follow-gate aren't supported.
If the upstream returns a video URL but no captions, you'll get a 200 with `transcript: ""` and a `videoUrls` object — and you'll be charged 1 credit. If the upstream returns no captions and no video URL, you'll get a `404` with `error: "No transcript available for this video"` and you won't be charged.
Live streams aren't supported. Once a Live is saved as a regular video and is publicly viewable, it works like any other URL.
Yes. We resolve them server-side — you don't need to expand the link before calling the API.
No — the TikTok response shape is intentionally lean: `transcript`, optional `videoUrls`, and `credits`. If you need the author handle, parse it from the URL you submitted.
# X
Source: https://docs.transcriptmagic.com/api-reference/twitter
Fetch transcripts from public video tweets on X (formerly Twitter).
```http theme={null}
POST https://api.transcriptmagic.com/api/twitter/transcript
```
Public video tweets are supported. Both `x.com` and `twitter.com` `/status/` URLs work. The video must be **under 2 minutes** — X tweets are AI-transcribed, so this endpoint is slower than the others.
## Request
```bash cURL theme={null}
curl -X POST https://api.transcriptmagic.com/api/twitter/transcript \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://x.com/TheoVon/status/1916982720317821050"}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.transcriptmagic.com/api/twitter/transcript",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"url": "https://x.com/TheoVon/status/1916982720317821050"},
)
data = response.json()
print(data["transcript"]) # plain string, newline-separated
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.transcriptmagic.com/api/twitter/transcript", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://x.com/TheoVon/status/1916982720317821050" }),
});
const data = await response.json();
console.log(data.transcript); // plain string, newline-separated
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"url": "https://x.com/TheoVon/status/1916982720317821050",
})
req, _ := http.NewRequest("POST",
"https://api.transcriptmagic.com/api/twitter/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 data struct {
Transcript string `json:"transcript"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data.Transcript)
}
```
```php PHP theme={null}
'https://x.com/TheoVon/status/1916982720317821050',
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo $data['transcript'];
```
## Response
```json theme={null}
{
"transcript": "Hey everyone, today I want to walk\nyou through how we built the new product line...",
"credits": 994
}
```
`transcript` is a plain string with newlines between spoken lines — there is no per-line timing in the X response and no `videoUrls` field.
## Supported URL formats
| Format | Example |
| -------------------- | ---------------------------------------- |
| `x.com` status | `https://x.com/{user}/status/{id}` |
| `twitter.com` status | `https://twitter.com/{user}/status/{id}` |
Both the `x.com` and legacy `twitter.com` domains are accepted as long as the URL points at a `/status/{id}` tweet. URLs without a `/status/` path are rejected with `400 Invalid URL`.
## FAQ
Yes. Both the `x.com` and the legacy `twitter.com` `/status/` URLs are accepted.
Yes — the video must be **under 2 minutes**. Longer videos can't be transcribed by this endpoint.
X tweets don't ship with captions, so the video is AI-transcribed on demand. That takes longer than the caption-based endpoints — expect a longer round trip, especially near the 2-minute limit.
Only tweets that contain a video. Text, image, and link tweets have nothing to transcribe and are rejected.
No. X transcripts are returned as plain text with newlines between lines — there is no per-line timing.
# YouTube
Source: https://docs.transcriptmagic.com/api-reference/youtube
POST any YouTube URL — Shorts, long-form, live replays — and get back a JSON transcript with per-line timing.
```http theme={null}
POST https://api.transcriptmagic.com/api/youtube/transcript
```
Works on watch URLs, `youtu.be` short links, `/shorts/` paths, and `m.youtube.com` mobile URLs.
## Request
```bash cURL theme={null}
curl -X POST https://api.transcriptmagic.com/api/youtube/transcript \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.transcriptmagic.com/api/youtube/transcript",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
)
data = response.json()
# transcript is an array of timed segments
for seg in data["transcript"]:
print(seg["startTimeText"], seg["text"])
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.transcriptmagic.com/api/youtube/transcript", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }),
});
const data = await response.json();
// transcript is an array of timed segments
for (const seg of data.transcript) {
console.log(seg.startTimeText, seg.text);
}
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
})
req, _ := http.NewRequest("POST",
"https://api.transcriptmagic.com/api/youtube/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 data struct {
Transcript []struct {
Text string `json:"text"`
StartMs string `json:"startMs"`
EndMs string `json:"endMs"`
StartTimeText string `json:"startTimeText"`
} `json:"transcript"`
TranscriptOnlyText string `json:"transcript_only_text"`
Language string `json:"language"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data.TranscriptOnlyText)
}
```
```php PHP theme={null}
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
// transcript is an array of timed segments
foreach ($data['transcript'] as $seg) {
echo $seg['startTimeText'] . ' ' . $seg['text'] . "\n";
}
```
## Response
```json theme={null}
{
"success": true,
"type": "video",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"transcript": [
{
"text": "We're no strangers to love",
"startMs": "18800",
"endMs": "25960",
"startTimeText": "0:18"
},
{
"text": "You know the rules and so do I",
"startMs": "21800",
"endMs": "29119",
"startTimeText": "0:21"
}
],
"transcript_only_text": "We're no strangers to love. You know the rules and so do I...",
"language": "English",
"videoId": "dQw4w9WgXcQ",
"captionTracks": [
{
"baseUrl": "https://www.youtube.com/api/timedtext?...",
"name": { "simpleText": "English" },
"languageCode": "en",
"isTranslatable": true
}
],
"credits": 997
}
```
The YouTube endpoint returns the per-line timed array directly as `transcript` — there is no separate `segments` field. If you just want the text without timings, read `transcript_only_text` instead.
## Supported URL formats
| Format | Example |
| --------------------------- | ---------------------------------------- |
| Standard watch URL | `https://www.youtube.com/watch?v=…` |
| YouTube Shorts | `https://www.youtube.com/shorts/…` |
| `youtu.be` short link | `https://youtu.be/…` |
| Mobile URL | `https://m.youtube.com/watch?v=…` |
| Live replay (with captions) | Any of the above, completed live streams |
## FAQ
Yes. Any public YouTube URL works — Shorts (`youtube.com/shorts/…`), long-form videos, live replays, mobile links, and `youtu.be` short links are all accepted. Just POST the full URL.
Unlisted videos work as long as you provide the full URL. Age-restricted videos that require sign-in are **not** supported — the API can only fetch publicly viewable captions.
Whichever caption track YouTube exposes — that covers 100+ languages for auto-captions and every language creators manually publish. The `language` field on the response tells you which track was returned, and `captionTracks` lists every available track for the video. Translation is a separate step you run after fetching.
Yes. Each item in the `transcript` array includes `startMs`, `endMs`, and `startTimeText` — build SRT/VTT, chaptered summaries, or jump-to-moment UI directly from the response. See the [response schema](/api-reference/response-schema) page for an SRT generator example.
When the creator has uploaded manual captions, accuracy is essentially 100%. When only YouTube auto-captions exist, accuracy matches YouTube's own ASR — very good for clear speech, weaker on heavy music.
# Authentication
Source: https://docs.transcriptmagic.com/authentication
Bearer token. That's it.
Every request to the TranscriptMagic API requires an API key, passed in the `Authorization` header. Each successful call deducts 1 credit from your account balance.
## The header
```http theme={null}
Authorization: Bearer sk_live_your_api_key_here
```
That's the whole protocol. No OAuth dance, no signed requests, no nonces. All keys begin with the `sk_live_` prefix.
## Getting a key
Create, view, and rotate keys on the [API keys page](https://transcriptmagic.com/dashboard/api-keys/) in the dashboard. You can have multiple active keys at once — useful for separating environments (staging vs. production) or scoping access to a specific service.
## Rotation and revocation
Revocation is **instant**. The moment you delete a key in the dashboard, every in-flight request using it returns `401 Unauthorized`. Recommended pattern for rotation:
1. Create the new key.
2. Deploy the new key to your environment.
3. Confirm requests succeed with the new key — the cheapest test is [`GET /api/balance`](/api-reference/balance), which validates the key without consuming a credit:
```bash theme={null}
curl https://api.transcriptmagic.com/api/balance \
-H "Authorization: Bearer sk_live_your_new_key"
```
4. Delete the old key.
## What requires auth
Every endpoint under `/api/` requires a valid bearer token starting with `sk_live_`. Requests without the header (or with a non-`sk_live_` token) fall through to the anonymous code path and return `400` for missing device ID. Requests with a malformed or revoked `sk_live_` key return `401 Unauthorized` with body:
```json theme={null}
{ "error": "Invalid API key" }
```
Once authenticated, requests against an account with **no remaining credits** return `403 Forbidden`:
```json theme={null}
{
"error": "no_credits",
"credits": 0,
"message": "Out of credits. Please upgrade to continue."
}
```
Note that the `error` field is the machine-readable slug `"no_credits"`, not a sentence — match on it programmatically. See [errors](/concepts/errors) for the full list.
## Treat keys like secrets
API keys grant access to your credit balance. Keep them out of client-side code, public repos, and screenshots. Use environment variables or a secret manager. If you suspect a key is compromised, delete it immediately in the dashboard and create a new one.
# Credits
Source: https://docs.transcriptmagic.com/concepts/credits
How API billing works — one credit per successful call.
The TranscriptMagic API uses a simple credit model: every successful response (HTTP 200) deducts exactly **1 credit** from your account balance. Length of the video doesn't matter — a 30-second YouTube Short and a 4-hour livestream replay each cost 1 credit.
## Free credits
Every account starts with free credits — enough to integrate, test, and ship a small project before you ever need to pay. No card required to sign up.
## Buying more
Top up on the [credits page](https://transcriptmagic.com/credits/). Pricing is volume-tiered; larger packs come with a lower per-credit cost.
## Checking your balance
Every authenticated API response includes a `credits` field showing your **remaining** balance after that call:
```json theme={null}
{
"transcript": "...",
"credits": 997
}
```
You don't need a separate billing call — every transcript response is also a balance check. For an explicit read without consuming a credit (pre-flight checks, CI smoke tests, UI shells), call [`GET /api/balance`](/api-reference/balance).
## Cache hits don't charge
If you request a URL that another user has already transcribed within the cache window, you get a 200 with `cached: true` in the body and **no credit is deducted**. Your `credits` value is unchanged on the response.
```json theme={null}
{
"transcript": "...",
"cached": true,
"credits": 997
}
```
## What doesn't cost credits
* Failed requests (`4xx`/`5xx` responses) — you're never charged for an error.
* Auth errors, rate-limit errors, upstream-platform errors — none of them deduct.
* Cache hits, as described above.
You only pay for transcripts you actually receive fresh.
## Running out
When your balance hits 0, the next request returns `403 Forbidden`:
```json theme={null}
{
"error": "no_credits",
"credits": 0,
"message": "Out of credits. Please upgrade to continue."
}
```
Top up at [/credits](https://transcriptmagic.com/credits/) and the next call goes through immediately — no key change needed. Match on the `error` slug `"no_credits"`, not the human-readable `message`.
## Need bulk pricing?
If you're processing more than \~10k transcripts a month, email [hello@transcriptmagic.com](mailto:hello@transcriptmagic.com) — we offer custom pricing and lifted rate limits for verified production accounts.
# Errors
Source: https://docs.transcriptmagic.com/concepts/errors
Status codes you might see, and what they mean.
Every error response is a JSON body with an `error` field. The `error` value is sometimes a machine-readable slug (e.g. `"no_credits"`, `"limit_reached"`) and sometimes a human-readable sentence — see the table below for which is which. Status codes are consistent across all four platform endpoints.
```json theme={null}
{ "error": "Description or slug" }
```
## Status codes
| Code | Name | When it fires | `error` field |
| ----- | -------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `400` | Bad Request | Invalid JSON body, missing `url`, or `url` doesn't match the expected platform format | `Invalid JSON body` / `Missing "url" in request body` / `Invalid URL. Please provide a valid video URL.` |
| `401` | Unauthorized | Missing, malformed, or revoked API key | `Invalid API key` |
| `403` | Forbidden | Account out of credits | `no_credits` (slug) — body also includes `credits: 0` and a human-readable `message` |
| `404` | Not Found | Upstream platform reported the video doesn't exist, is private, or has no transcript available | `Video not found` / `Shoot the post does not have a video` / `No transcript available for this video` (varies — comes from upstream) |
| `429` | Rate Limited | 120 req/min cap exceeded for this key | `Rate limit exceeded. Try again later.` — see [rate limits](/concepts/rate-limits) |
| `500` | Internal Error | Unexpected worker error, or `SCRAPECREATORS_API_KEY` unset on the server side | `Internal server error` / `API key not configured` |
| `502` | Bad Gateway | Upstream platform API returned a non-2xx HTTP status | `Failed to fetch transcript` — body also includes a `details` string echoing the upstream payload |
404 vs 502: a `502` means the upstream HTTP request itself failed (network error, upstream 5xx, etc.). A `404` means the upstream returned 200 but flagged the video as not retrievable (deleted, private, no captions). Treat both as "this video can't be transcribed today" — but only `502` is worth retrying.
## Retry guidance
Retry only on `429` and `502`. Use exponential backoff with jitter, and cap retries at 3–5 attempts. Don't retry on other `4xx` codes — the request itself is wrong, retrying won't fix it.
A common pattern:
```python theme={null}
import time, random, requests
def call_with_backoff(url, payload, headers, max_retries=4):
for attempt in range(max_retries):
r = requests.post(url, json=payload, headers=headers)
if r.status_code < 400:
return r
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "1")))
continue
if r.status_code == 502:
time.sleep((2 ** attempt) + random.uniform(0, 1))
continue
# Non-retriable error (400, 401, 403, 404, 500)
r.raise_for_status()
r.raise_for_status()
```
## Reporting issues
If you hit a status code that doesn't match the table above, or you see persistent `5xx` errors on a URL you believe should work, email [hello@transcriptmagic.com](mailto:hello@transcriptmagic.com) with the URL and approximate timestamp — we'll investigate.
# Rate limits
Source: https://docs.transcriptmagic.com/concepts/rate-limits
One hundred twenty requests per minute, per key.
Every API key is allowed **120 requests per minute**. The window is fixed (not a sliding window): the first request opens a 60-second bucket, and the counter resets when that bucket expires. The limit is enforced per-key — so you can scale horizontally by issuing additional keys to separate workers.
## Headers
Every response from an API-key-authenticated request includes the rate-limit state, so you can self-throttle without waiting to hit a 429.
| Header | Description |
| ----------------------- | ------------------------------------------------------------------- |
| `X-RateLimit-Limit` | Your per-minute cap. `120` for all standard accounts. |
| `X-RateLimit-Remaining` | Requests left in the current window after this call. |
| `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets. |
| `Retry-After` | Seconds to wait before retrying. **Only present on 429 responses.** |
The headers fire on every status code — `200`, `400`, `404`, `429`, `502`, etc. — as long as the request authenticated via API key. Anonymous (web-form) and Google-session-token requests do not receive these headers because they're not subject to the same per-key quota.
## Handling 429
When you exceed the limit, the API returns:
```json theme={null}
{ "error": "Rate limit exceeded. Try again later." }
```
…with a `Retry-After` header. Wait that many seconds before your next request. Don't poll faster — repeated 429s won't shorten the wait, they just waste sockets.
```python theme={null}
import time, requests
def call_with_retry(url, payload, headers):
while True:
r = requests.post(url, json=payload, headers=headers)
if r.status_code != 429:
return r
wait = int(r.headers.get("Retry-After", "1"))
time.sleep(wait)
```
## Self-throttling
Watch `X-RateLimit-Remaining` on normal traffic and slow down before you hit the wall. A small headroom (e.g. pause when remaining drops below 10) absorbs the slop from in-flight requests that haven't decremented yet. For higher concurrency, issue a separate key per worker — each key has its own independent 120/min bucket.
## Higher limits
Need more than 120/min? Email [hello@transcriptmagic.com](mailto:hello@transcriptmagic.com) — we lift caps to **600/min** for verified production accounts.
# Introduction
Source: https://docs.transcriptmagic.com/introduction
Programmatic access to video transcripts from YouTube, TikTok, Instagram, Facebook, LinkedIn, Rumble, and X — one REST call, JSON response.
The TranscriptMagic API returns transcripts from public video URLs on the seven platforms we support. One auth scheme, seven paths, and a per-platform response shape (YouTube returns timed segments; TikTok and Facebook return joined text plus optional download URLs; Instagram, LinkedIn, Rumble, and X return plain text).
## When to use the API
Reach for the API when you're building automation: ingesting channel archives into a vector DB, generating SRT files at scale (YouTube only — it's the platform that returns per-line timing), indexing video courses, or wiring transcripts into an LLM pipeline. If you just need to grab a single transcript, the [no-code tools](https://transcriptmagic.com) on the main site are faster.
If you're working **inside an AI client** (Claude, ChatGPT, Cursor, Claude Code), connect the [MCP server](/mcp-server) instead — OAuth sign-in, no API key, tools available in every chat. If you orchestrate workflows in n8n, install the [community node](/n8n) and skip the glue code.
## Supported platforms
Watch URLs, `youtu.be` short links, `/shorts/`, mobile, and live replays. Returns timed segments.
Full share URLs and `vm.tiktok.com` short links resolve cleanly.
Reels (`/reel/`), video posts (`/p/`), and IGTV (`/tv/`). Stories are not supported (no stable URLs).
Reels, Watch, posts containing video, video posts, and `fb.watch` short links — all formats accepted.
Public posts containing video. Returns plain text.
Public videos that have captions. WEBVTT cleaned to plain text.
Video tweets under 2 minutes. AI-transcribed, so slower than the others.
## Pricing
Every successful API call costs **1 credit** — whether the video is a 30-second Short or a 4-hour livestream replay. Free credits are included with every account, no card required to start. Buy more anytime on the [credits page](https://transcriptmagic.com/credits/). Cache hits and error responses are free.
## A taste
```bash theme={null}
curl -X POST https://api.transcriptmagic.com/api/youtube/transcript \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}'
```
Next: [generate a key](/quickstart) and make your first call.
# MCP Server
Source: https://docs.transcriptmagic.com/mcp-server
Connect TranscriptMagic to Claude, ChatGPT, Cursor, and other AI clients via the Model Context Protocol — no API key, OAuth sign-in.
## Overview
The TranscriptMagic MCP server gives any MCP-compatible AI client a one-click set of tools to fetch video transcripts from YouTube, TikTok, Instagram, Facebook, LinkedIn, Rumble, and X (Twitter), transcribe Spotify and Apple Podcasts episodes — plus check credit balance and recall transcript history.
It is a **remote MCP server over Streamable HTTP** with **OAuth 2.1** authentication. There is no API key to copy — users sign in once with Google when they connect, and the AI client manages its own short-lived token.
```
https://mcp.transcriptmagic.com/mcp
```
If you are building a service or batch pipeline (no AI client in the loop), the [REST API](/api-reference/overview) is a better fit. Same backend, same credits.
## When to use MCP vs. the REST API
| Use MCP when | Use the REST API when |
| ----------------------------------------------------------------- | ----------------------------------------------------- |
| You're inside a chat with Claude, ChatGPT, Cursor, or Claude Code | You're writing a service, agent runtime, or batch job |
| You want zero glue code | You want a predictable JSON contract |
| Your users need transcripts ad-hoc, in plain English | You need per-line YouTube timestamps |
| You don't want to manage API keys | You're integrating into a non-AI product |
## Tools
The server exposes eleven tools.
### Transcribe tools (1 credit each)
Fetch the transcript of a public YouTube video given its URL.
**Input**
* `url` (string, required) — full YouTube URL. Accepts `youtube.com/watch?v=…`, `youtu.be/…`, `youtube.com/shorts/…`, and `m.youtube.com` mobile links.
**Cost** 1 credit per successful call.
**Notes** Returns plain text. Per-line timestamps are available in the [REST API](/api-reference/youtube) response, not in the MCP text output.
Fetch the transcript of a public TikTok video.
**Input**
* `url` (string, required) — full `tiktok.com/@user/video/…` URL or `vm.tiktok.com/…` short link.
**Cost** 1 credit per successful call.
Fetch the transcript of a public Instagram video.
**Input**
* `url` (string, required) — `instagram.com/reel/…`, `instagram.com/p/…`, or `instagram.com/tv/…`.
**Cost** 1 credit per successful call.
Fetch the transcript of a public Facebook video.
**Input**
* `url` (string, required) — `facebook.com/watch?v=…`, `fb.watch/…`, `facebook.com/reel/…`, or Live replay URL.
**Cost** 1 credit per successful call.
Fetch the transcript of a public LinkedIn post with video.
**Input**
* `url` (string, required) — `linkedin.com/posts/…` or `linkedin.com/feed/update/…`.
**Cost** 1 credit per successful call.
**Notes** Works on public posts only; login-walled or member-private posts can't be fetched.
Fetch the transcript of a Rumble video with captions.
**Input**
* `url` (string, required) — full `rumble.com/…` video URL.
**Cost** 1 credit per successful call.
**Notes** Requires the video to have captions available.
Fetch the transcript of a video posted on X (Twitter).
**Input**
* `url` (string, required) — `x.com/…/status/…` or `twitter.com/…/status/…`.
**Cost** 1 credit per successful call.
**Notes** Works on video tweets under 2 minutes.
### Podcast tools (Plus & Pro · 10 credits per audio-hour)
Transcribe a Spotify or Apple Podcasts episode. Podcast audio is transcribed with AI speech-to-text (episodes rarely ship captions), so this runs asynchronously: short episodes return the transcript inline, while longer ones return a `job_id` to check with `get_podcast_status`.
**Input**
* `url` (string, required) — a Spotify episode (`open.spotify.com/episode/…`) or Apple Podcasts episode (`podcasts.apple.com/…?i=…`) link.
**Cost** 10 credits per audio-hour, on the Plus and Pro plans. Reserved when the job starts and refunded automatically if it fails.
**Notes** On the Free plan or a legacy one-time credit pack the tool returns an upgrade prompt.
Check a podcast job started by `transcribe_podcast` and return the transcript once it's ready.
**Input**
* `job_id` (string, required) — the id returned by `transcribe_podcast` for a longer episode.
**Cost** Free to poll; the transcription itself is billed by `transcribe_podcast`.
### Utility tools (free)
Returns the signed-in user's remaining transcript credits and current plan.
**Input** none.
**Cost** Free; does not consume credits.
Lists the user's recently saved transcripts (most recent first). Useful when the AI wants to recall something the user already fetched.
**Input**
* `limit` (integer, 1–50, optional, default 10) — max number of transcripts to return.
* `platform` (`youtube` | `tiktok` | `instagram` | `facebook` | `linkedin` | `rumble` | `twitter`, optional) — filter to a single platform.
**Cost** Free; does not consume credits. Each item includes the video URL, platform, title, and a short text preview.
## Setup by client
1. Open Claude Desktop → **Settings** → **Connectors**.
2. Click **Add custom connector**.
3. Paste the server URL: `https://mcp.transcriptmagic.com/mcp`.
4. A browser tab opens — sign in with Google and click **Approve**.
5. The tools become available in every chat. Try: *"Summarize this YouTube video: \"*.
1. Open [claude.ai/settings/connectors](https://claude.ai/settings/connectors).
2. Click **Add custom connector**.
3. Paste the server URL: `https://mcp.transcriptmagic.com/mcp`.
4. Sign in with Google in the popup, then click **Approve**.
1. Open ChatGPT → **Settings** → **Connectors** → **Add**.
2. Paste the server URL and follow the OAuth prompt.
3. Available in any chat with deep research enabled.
Open **Settings** → **MCP** → **Add new MCP server** and paste:
```json theme={null}
{
"mcpServers": {
"TranscriptMagic": {
"url": "https://mcp.transcriptmagic.com/mcp"
}
}
}
```
In your terminal:
```bash theme={null}
claude mcp add --transport http TranscriptMagic https://mcp.transcriptmagic.com/mcp
```
Claude Code will open a browser tab for OAuth the first time you use a tool.
You can also manage connections from the [dashboard MCP page](https://transcriptmagic.com/dashboard/mcp) — view connected clients and revoke them.
## Authentication
The server implements OAuth 2.1 (RFC 8414 + dynamic client registration). The flow looks like this:
1. The MCP client requests the server's authorization metadata.
2. It registers itself dynamically and redirects the user to TranscriptMagic's consent screen.
3. The user signs in with Google and approves access.
4. The server issues an access token scoped to the client; the user's TranscriptMagic account, credits, and history are linked.
Tokens are short-lived; clients refresh them automatically. There is nothing for end users to copy or paste.
## Errors
The MCP server returns errors as `isError: true` content blocks the AI can read directly. Common cases:
* **Out of credits** — *"Out of credits. Top up or upgrade at transcriptmagic.com/dashboard/account."*
* **Invalid URL** — the URL didn't match the platform's expected format.
* **No transcript available** — public video, but no spoken audio or captions.
* **Private / login-walled** — the video can't be fetched without sign-in.
The AI will typically retry or ask the user for a different URL on its own.
## Pricing
Free credits are included on signup. After that, **1 credit per successful `transcribe_*` call**. Podcast transcription (`transcribe_podcast`) is a **Plus/Pro** feature billed at **10 credits per audio-hour**. Utility tools (`get_credit_balance`, `list_recent_transcripts`, `get_podcast_status`) are free.
See the [pricing page](https://transcriptmagic.com/credits) for plan details.
## Related
Fully documented REST endpoints for non-MCP integrations.
Bearer-token auth for the REST API (separate from MCP OAuth).
Status codes and retry strategy for the REST API.
120 req/min on the REST API. MCP is rate-limited only by your credit balance.
# n8n Node
Source: https://docs.transcriptmagic.com/n8n
Fetch video transcripts directly inside n8n workflows. Community node, published to npm, API-key auth.
## Overview
`n8n-nodes-transcriptmagic` is a community node that wraps the TranscriptMagic API as drag-and-drop operations inside [n8n](https://n8n.io). Drop it into a workflow, point it at a public video URL, and the transcript flows through to the next node like any other JSON.
It is published to npm (currently **v0.2.0**) and works in both self-hosted n8n and n8n Cloud (once verified).
```
n8n-nodes-transcriptmagic
```
If you're writing a service or batch job in code, use the [REST API](/api-reference/overview) directly. If you're inside an AI client, use the [MCP server](/mcp-server). Use n8n when your workflow orchestration already lives in n8n.
## Install
1. Open **Settings** → **Community Nodes** → **Install**.
2. Enter `n8n-nodes-transcriptmagic` and confirm.
3. The node appears in the picker under **TranscriptMagic**.
n8n Cloud requires verified community nodes. If the install button is missing, the package is awaiting verification — self-host in the meantime.
From your n8n install directory:
```bash theme={null}
npm install n8n-nodes-transcriptmagic
```
Restart n8n. The node appears under **TranscriptMagic** in the node picker.
Docker users: mount the package into `/home/node/.n8n/custom/` and restart the container.
## Setup
1. [Create an API key](https://transcriptmagic.com/dashboard/api-keys/) in your TranscriptMagic dashboard. Keys begin with `sk_live_`.
2. In n8n, add a new **TranscriptMagic API** credential and paste your key. The credential test calls [`GET /api/balance`](/api-reference/balance) — it validates the key without consuming a credit, and works even on zero-credit accounts.
## Operations
### Transcript
POST a video URL, receive the transcript.
| Operation | Endpoint |
| ----------- | -------------------------------- |
| YouTube | `POST /api/youtube/transcript` |
| TikTok | `POST /api/tiktok/transcript` |
| Instagram | `POST /api/instagram/transcript` |
| Facebook | `POST /api/facebook/transcript` |
| LinkedIn | `POST /api/linkedin/transcript` |
| Rumble | `POST /api/rumble/transcript` |
| X (Twitter) | `POST /api/twitter/transcript` |
**Cost** 1 credit per successful call. Cache hits and errors are free.
### Account
| Operation | Endpoint |
| ------------------ | ------------------ |
| Get Credit Balance | `GET /api/balance` |
Free; useful as a pre-flight check before a big batch.
## Output formats
Set via **Options → Output Format**:
* **Normalized** (default) — `{ text, platform, credits, url }`. Hides per-platform shape differences so a single downstream branch works across all sources.
* **Raw** — the upstream API response passed through verbatim. Use this when you need YouTube's per-line timed segments or platform-specific metadata (`videoUrls`, `language`, etc.). See the [response schema](/api-reference/response-schema).
## Error handling
* **Continue on fail** is supported. A bad URL in a batch surfaces as a per-item error and the rest of the batch keeps running.
* **Rate limits** — when the API returns `429`, the node honors `Retry-After` and retries once. The per-key limit is 120 requests/minute. See [rate limits](/concepts/rate-limits).
## Common patterns
The minimal flow: trigger → transcribe → done.
1. **Manual Trigger**
2. **TranscriptMagic** — Resource `Transcript`, Operation `YouTube` (or TikTok / Instagram / Facebook / LinkedIn / Rumble / X (Twitter)), URL = any public video URL.
3. Run. The output item contains `text`, `platform`, `credits`, and `url`.
Process many URLs without aborting the batch on a single bad URL.
1. **Google Sheets** / **Airtable** / **Set** node returning items with a `url` field.
2. **TranscriptMagic** — URL = `={{ $json.url }}`, **Settings → On Error** = Continue.
3. Downstream destination keyed off the original `url` from the paired item.
The node automatically honors `Retry-After` on 429.
The classic insight-extraction pattern.
1. **Schedule Trigger** (or **Webhook**, **RSS Feed**, **Telegram**, etc.)
2. **TranscriptMagic** → fetch transcript.
3. **OpenAI** / **Anthropic** node — prompt with `={{ $json.text }}`.
4. **Slack** / **Email** / **Notion** → deliver the summary.
Before kicking off a big batch, verify you have credits.
1. **TranscriptMagic** — Resource `Account`, Operation `Get Credit Balance`.
2. **IF** — `={{ $json.credits >= 1000 }}` → continue, else send an alert.
The balance call does not consume a credit.
## Links
Issues, releases, and example workflows.
The underlying API the node calls — useful for advanced cases.
How API keys work and how to rotate them.
Status codes and retry strategy.
# Quickstart
Source: https://docs.transcriptmagic.com/quickstart
From key to transcript in under five minutes.
Three steps: generate a key in the dashboard, POST a video URL, parse the JSON. No SDK, no auth dance, no webhook plumbing.
Generate one in your [dashboard](https://transcriptmagic.com/dashboard/api-keys/). Free credits are included — no card to start.
Send any supported URL in the `url` field. Auth via the `Authorization: Bearer` header.
```bash cURL theme={null}
curl -X POST https://api.transcriptmagic.com/api/youtube/transcript \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}'
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.transcriptmagic.com/api/youtube/transcript",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
)
data = response.json()
# YouTube returns transcript as an array of timed segments
for seg in data["transcript"]:
print(seg["startTimeText"], seg["text"])
```
```javascript JavaScript theme={null}
const response = await fetch("https://api.transcriptmagic.com/api/youtube/transcript", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }),
});
const data = await response.json();
// YouTube returns transcript as an array of timed segments
for (const seg of data.transcript) {
console.log(seg.startTimeText, seg.text);
}
```
```go Go theme={null}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]string{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
})
req, _ := http.NewRequest("POST",
"https://api.transcriptmagic.com/api/youtube/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 data struct {
Transcript []struct {
Text string `json:"text"`
StartMs string `json:"startMs"`
EndMs string `json:"endMs"`
StartTimeText string `json:"startTimeText"`
} `json:"transcript"`
TranscriptOnlyText string `json:"transcript_only_text"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data.TranscriptOnlyText)
}
```
```php PHP theme={null}
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
]));
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
// YouTube returns transcript as an array of timed segments
foreach ($data['transcript'] as $seg) {
echo $seg['startTimeText'] . ' ' . $seg['text'] . "\n";
}
```
The exact shape varies by platform. YouTube returns timed segments; TikTok and Facebook return the joined text plus optional video URLs; Instagram, LinkedIn, Rumble, and X return plain text. Sample YouTube response:
```json theme={null}
{
"success": true,
"type": "video",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"transcript": [
{ "text": "We're no strangers to love", "startMs": "18800", "endMs": "25960", "startTimeText": "0:18" },
{ "text": "You know the rules and so do I", "startMs": "21800", "endMs": "29119", "startTimeText": "0:21" }
],
"transcript_only_text": "We're no strangers to love. You know the rules and so do I...",
"language": "English",
"videoId": "dQw4w9WgXcQ",
"captionTracks": [/* available caption tracks */],
"credits": 42
}
```
Full per-platform shapes live in the [response schema](/api-reference/response-schema).
## Swap platforms
Auth, path style, and the `url` body field never change — but the **response shape differs per platform** because each upstream source provides different metadata. YouTube gives you per-line timing; TikTok and Facebook give you joined text plus optional video URLs; Instagram gives you a `transcripts` array of plain-text lines; LinkedIn, Rumble, and X give you a plain `transcript` string. Plan for that when writing a unified client.
## Next steps
Bearer tokens, key rotation, revocation.
Every field, every platform.
120/min per key. Headers on 429 responses.
Status codes, messages, retry guidance.