Quickstart by language
No SDK required. Every example is raw HTTP and works on any runtime that can make a request.
TypeScript / JavaScript (fetch)
const res = await fetch("https://transcribe.so/api/v1/transcriptions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TRANSCRIBE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
source: "youtube",
url: "https://youtu.be/dQw4w9WgXcQ",
pipeline_code: "qwen3-asr-flash-filetrans",
}),
});
const job = await res.json();
console.log("queued", job.id);Works on Node, Bun, Deno, Cloudflare Workers, browsers (CORS is open).
Python (requests)
import os, requests
job = requests.post(
"https://transcribe.so/api/v1/transcriptions",
headers={"Authorization": f"Bearer {os.environ['TRANSCRIBE_API_KEY']}"},
json={
"source": "youtube",
"url": "https://youtu.be/dQw4w9WgXcQ",
"pipeline_code": "qwen3-asr-flash-filetrans",
},
).json()
print("queued", job["id"])Bash (curl)
curl -sS -X POST https://transcribe.so/api/v1/transcriptions \
-H "Authorization: Bearer $TRANSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "youtube",
"url": "https://youtu.be/dQw4w9WgXcQ",
"pipeline_code": "qwen3-asr-flash-filetrans"
}'Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]any{
"source": "youtube",
"url": "https://youtu.be/dQw4w9WgXcQ",
"pipeline_code": "qwen3-asr-flash-filetrans",
})
req, _ := http.NewRequest("POST", "https://transcribe.so/api/v1/transcriptions", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("TRANSCRIBE_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var job struct{ Id string }
json.NewDecoder(res.Body).Decode(&job)
fmt.Println("queued", job.Id)
}Ruby
require "net/http"
require "json"
uri = URI("https://transcribe.so/api/v1/transcriptions")
req = Net::HTTP::Post.new(uri, {
"Authorization" => "Bearer #{ENV['TRANSCRIBE_API_KEY']}",
"Content-Type" => "application/json",
})
req.body = {
source: "youtube",
url: "https://youtu.be/dQw4w9WgXcQ",
pipeline_code: "qwen3-asr-flash-filetrans",
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
puts "queued", JSON.parse(res.body)["id"]PHP
<?php
$body = json_encode([
"source" => "youtube",
"url" => "https://youtu.be/dQw4w9WgXcQ",
"pipeline_code" => "qwen3-asr-flash-filetrans",
]);
$ch = curl_init("https://transcribe.so/api/v1/transcriptions");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("TRANSCRIBE_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_RETURNTRANSFER => true,
]);
$job = json_decode(curl_exec($ch), true);
echo "queued " . $job["id"];AI agents
Modern agent frameworks call HTTP APIs natively — no SDK or MCP server required for most.
Claude Code (terminal-based)
Claude Code's bash tool can call our API directly. Drop this in your CLAUDE.md so the model knows the convention:
# Transcribe a URL with transcribe.so
curl -sS -X POST https://transcribe.so/api/v1/transcriptions \
-H "Authorization: Bearer $TRANSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"source":"youtube","url":"...","pipeline_code":"qwen3-asr-flash-filetrans"}'Cursor / Cline / Continue / Aider
Same pattern — Cursor/Cline/Continue/Aider all expose a terminal tool and can run the curl above. Hand the agent a snippet of your wrapper script and it'll figure out polling.
ChatGPT Custom GPT (Actions)
Create a Custom GPT, add an Action, paste this OpenAPI URL:
https://transcribe.so/api/v1/openapi.yamlSet authentication to API Key → Authentication Type Bearer. ChatGPT users paste their tsk_live_… key once and the GPT can transcribe URLs end-to-end.
Claude Desktop (MCP) — coming soon
MCP server @transcribe-so/mcp is on the roadmap. Until then, Claude Desktop users can call the API via Claude Code or via a small wrapper Shortcut on macOS. Watch the launch post for updates.
Generic agent (webhook handler)
Most agents are stateless — kick off the job, return the ID, let a webhook fire when done. Verify the signature on your end:
import { createHmac, timingSafeEqual } from "crypto";
export function verifyTranscribeSignature(rawBody: string, header: string, secret: string) {
const m = header.match(/t=(\d+),v1=([0-9a-f]+)/);
if (!m) return false;
const [, t, v1] = m;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return expected.length === v1.length &&
timingSafeEqual(Buffer.from(expected, "utf8"), Buffer.from(v1, "utf8"));
}Mobile
iPhone — Shortcuts
We're publishing a Shortcut template that lets you tap Share → Transcribe with transcribe.so from any iOS app. Until the template ships, you can build your own in 10 minutes:
- Open the Shortcuts app → + → name it "Transcribe".
- Add Get Contents of URL. Method: POST. URL:
https://transcribe.so/api/v1/transcriptions. - Headers:
Authorization: Bearer tsk_live_…,Content-Type: application/json. - Request body (JSON):
{ "source": "external_url", "url": "<Shortcut Input>", "pipeline_code": "qwen3-asr-flash-filetrans" } - Add Show Notification with the job id.
- In Settings → "Use with Share Sheet" → enable for URLs.
Use source: "youtube" if the input URL is a YouTube link.
Android — Tasker
Tasker's HTTP Request action wires to our API in a single step:
- Method: POST
- URL:
https://transcribe.so/api/v1/transcriptions - Headers:
Authorization: Bearer tsk_live_… - Body:
{"source":"external_url","url":"%input","pipeline_code":"qwen3-asr-flash-filetrans"}
Pair with a Tasker profile that fires when a sharing intent contains a URL — that's your Android share-sheet equivalent.
Desktop
macOS — shell alias
# Add to ~/.zshrc
export TRANSCRIBE_API_KEY=tsk_live_...
transcribe() {
curl -sS -X POST https://transcribe.so/api/v1/transcriptions \
-H "Authorization: Bearer $TRANSCRIBE_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"source\":\"youtube\",\"url\":\"$1\",\"pipeline_code\":\"qwen3-asr-flash-filetrans\"}" \
| jq .
}
# Usage: transcribe "https://youtu.be/..."macOS — Raycast (extension coming)
Official Raycast extension is on the roadmap — single command "Transcribe URL" that lists recent jobs and opens results in browser. Until then, the shell alias above works inside Raycast's Script Commands.
macOS — Hammerspoon hotkey
-- ~/.hammerspoon/init.lua: Cmd+Shift+T transcribes whatever URL is on the clipboard.
hs.hotkey.bind({"cmd", "shift"}, "T", function()
local url = hs.pasteboard.getContents()
hs.task.new("/usr/bin/curl", function(ec, out, err)
hs.notify.new({title = "transcribe.so", informativeText = "queued: " .. (out or err)}):send()
end, {
"-sS", "-X", "POST",
"https://transcribe.so/api/v1/transcriptions",
"-H", "Authorization: Bearer " .. os.getenv("TRANSCRIBE_API_KEY"),
"-H", "Content-Type: application/json",
"-d", string.format('{"source":"youtube","url":"%s","pipeline_code":"qwen3-asr-flash-filetrans"}', url),
}):start()
end)Windows — PowerShell
$env:TRANSCRIBE_API_KEY = "tsk_live_..."
$body = @{
source = "youtube"
url = "https://youtu.be/dQw4w9WgXcQ"
pipeline_code = "qwen3-asr-flash-filetrans"
} | ConvertTo-Json
Invoke-RestMethod -Method POST `
-Uri "https://transcribe.so/api/v1/transcriptions" `
-Headers @{ Authorization = "Bearer $env:TRANSCRIBE_API_KEY" } `
-ContentType "application/json" `
-Body $bodyWindows — AutoHotkey hotkey
; AutoHotkey v2: Win+Shift+T transcribes the clipboard URL.
#+t::
{
url := A_Clipboard
body := Format('{"source":"youtube","url":"{1}","pipeline_code":"qwen3-asr-flash-filetrans"}', url)
RunWait('powershell -NoProfile -Command "Invoke-RestMethod -Method POST -Uri https://transcribe.so/api/v1/transcriptions -Headers @{Authorization=\"Bearer ' EnvGet('TRANSCRIBE_API_KEY') '\"} -ContentType application/json -Body \"' body '\""', , 'Hide')
TrayTip('transcribe.so', 'queued')
}Backend / serverless
Cloudflare Worker (with webhook verification)
// wrangler.toml — set TRANSCRIBE_API_KEY and TRANSCRIBE_WEBHOOK_SECRET
export default {
async fetch(req: Request, env: any) {
if (req.method === "POST" && new URL(req.url).pathname === "/webhook") {
const raw = await req.text();
const sig = req.headers.get("x-transcribe-signature") || "";
if (!(await verify(raw, sig, env.TRANSCRIBE_WEBHOOK_SECRET))) {
return new Response("invalid", { status: 401 });
}
const evt = JSON.parse(raw);
console.log("got", evt.event, evt.data?.transcription?.id);
return new Response("ok");
}
return new Response("ok");
},
};
async function verify(body: string, header: string, secret: string) {
const m = header.match(/t=(\d+),v1=([0-9a-f]+)/);
if (!m) return false;
const [, t, v1] = m;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) return false;
const enc = new TextEncoder();
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(`${t}.${body}`));
const expected = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, "0")).join("");
return expected.length === v1.length && expected === v1;
}Vercel / Next.js webhook receiver
// app/api/transcribe-webhook/route.ts
import { createHmac, timingSafeEqual } from "crypto";
export const runtime = "nodejs";
export async function POST(req: Request) {
const raw = await req.text();
const sig = req.headers.get("x-transcribe-signature") || "";
const m = sig.match(/t=(\d+),v1=([0-9a-f]+)/);
if (!m) return new Response("invalid", { status: 401 });
const [, t, v1] = m;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) {
return new Response("stale", { status: 401 });
}
const expected = createHmac("sha256", process.env.TRANSCRIBE_WEBHOOK_SECRET!)
.update(`${t}.${raw}`).digest("hex");
if (expected.length !== v1.length ||
!timingSafeEqual(Buffer.from(expected, "utf8"), Buffer.from(v1, "utf8"))) {
return new Response("invalid signature", { status: 401 });
}
const evt = JSON.parse(raw);
// ... do something with evt.data.transcription
return new Response("ok");
}AWS Lambda (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";
export const handler = async (event: any) => {
const raw = event.body;
const sig = event.headers["x-transcribe-signature"] || "";
const m = sig.match(/t=(\d+),v1=([0-9a-f]+)/);
if (!m) return { statusCode: 401, body: "invalid" };
const [, t, v1] = m;
const expected = createHmac("sha256", process.env.TRANSCRIBE_WEBHOOK_SECRET!)
.update(`${t}.${raw}`).digest("hex");
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
return { statusCode: 401, body: "invalid" };
}
const evt = JSON.parse(raw);
// ...
return { statusCode: 200, body: "ok" };
};n8n — HTTP Request node
- Method: POST
- URL:
https://transcribe.so/api/v1/transcriptions - Authentication: Header Auth → Name
Authorization→ ValueBearer tsk_live_… - Body Content Type: JSON
- JSON Body:
{ "source": "youtube", "url": "{{$json.url}}", "pipeline_code": "qwen3-asr-flash-filetrans" }
For the webhook side, use n8n's Webhook trigger and verify the X-Transcribe-Signature in a Function node with the same HMAC pattern as above.
Zapier / Make.com
Both treat the API as a generic Webhooks → POST integration. Same headers and JSON body as n8n. For receiving deliveries, use the platform's "Catch Hook" / "Webhooks - Custom" trigger and verify in a Code step.
Browser
Vanilla fetch from a SPA
CORS is open on every /api/v1/* endpoint, so you can call us directly from a browser. Don't ship your Bearer token to the client in production — proxy through a server route.
const res = await fetch("https://transcribe.so/api/v1/transcriptions", {
method: "POST",
headers: {
Authorization: "Bearer tsk_live_...", // server-side only in real apps
"Content-Type": "application/json",
},
body: JSON.stringify({
source: "youtube",
url: window.location.href,
pipeline_code: "qwen3-asr-flash-filetrans",
}),
});
console.log(await res.json());Bookmarklet — transcribe the current tab
Save this as a bookmark. Set your key once in localStorage (localStorage.transcribe_key = "tsk_live_…"), then click the bookmark on any YouTube page to enqueue a transcription.
javascript:(async()=>{const k=localStorage.transcribe_key;if(!k){alert("Set localStorage.transcribe_key first");return}const r=await fetch("https://transcribe.so/api/v1/transcriptions",{method:"POST",headers:{Authorization:"Bearer "+k,"Content-Type":"application/json"},body:JSON.stringify({source:"youtube",url:location.href,pipeline_code:"qwen3-asr-flash-filetrans"})});const j=await r.json();alert(j.error?j.error.message:"queued: "+j.id)})();Missing a recipe?
Tell us what you'd like to see — we ship recipes faster than proper SDKs and they're a lot easier to iterate on.