transcribe.so API · Cookbook

Recipes for every platform

Copy-pastable snippets for the platforms developers actually integrate from. Each recipe is small, tested, and uses the same Bearer-token API documented at /developers/docs.

Quickstart by language

No SDK required. Every example is raw HTTP and works on any runtime that can make a request.

TypeScript / JavaScript (fetch)

typescript
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)

python
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)

bash
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

typescript
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

python
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

typescript
<?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:

bash
# 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:

bash
https://transcribe.so/api/v1/openapi.yaml

Set 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:

typescript
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:

  1. Open the Shortcuts app → + → name it "Transcribe".
  2. Add Get Contents of URL. Method: POST. URL: https://transcribe.so/api/v1/transcriptions.
  3. Headers: Authorization: Bearer tsk_live_…, Content-Type: application/json.
  4. Request body (JSON): { "source": "external_url", "url": "<Shortcut Input>", "pipeline_code": "qwen3-asr-flash-filetrans" }
  5. Add Show Notification with the job id.
  6. 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

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

typescript
-- ~/.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

typescript
$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 $body

Windows — AutoHotkey hotkey

typescript
; 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)

typescript
// 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

typescript
// 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)

typescript
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 → Value Bearer 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.

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

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