txscribe

Transcribe un enlace de YouTube, Google Drive o Dropbox, luego busca tus grabaciones y lee transcripciones (txt, SRT, VTT o JSON con marcas de tiempo de palabras y hablantes) y resúmenes desde tu cuenta de txscribe. Alojado en https://txscribe.ai/mcp con inicio de sesión OAuth, o ejecútalo localmente con npx -y @txscribe/mcp y una clave de API.

Servidor MCP alojado

npx add-mcp 'https://txscribe.ai/mcp'

Se instala en Claude Code, Codex, Cursor y más

Documentación

Create transcriptions from links or uploaded files, read transcripts and summaries, and get a signed webhook when they finish.

The API is the app underneath: the same transcription, the same checks and the same limits. Everything you create through it appears in your library, and it uses your account's minutes exactly as an upload in the app does — there is no separate API allowance or price.

Base URL https://txscribe.ai/api/v1. The full description is OpenAPI 3.1 JSON.

Authentication

Make a key under Plan and billing → Developer API. It starts txs_live_ and is shown once: we keep only a hash of it, so a lost key cannot be shown again — revoke it and make another. An account can hold 5 keys.

Send it on every request as Authorization: Bearer txs_live_…. The API does not accept a signed-in browser session, and a key does not sign anyone in to the app.

Quick start

curl — transcribe a link

curl https://txscribe.ai/api/v1/transcriptions \
  -H "Authorization: Bearer $TXSCRIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url": "https://www.youtube.com/watch?v=VIDEO_ID", "language": "auto"}'

curl — upload a file

# 1. Create the transcription. The answer carries a one-time upload URL.
curl https://txscribe.ai/api/v1/transcriptions \
  -H "Authorization: Bearer $TXSCRIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fileName": "interview.m4a", "contentType": "audio/mp4", "size": 18234567}'

# 2. PUT the whole file to upload.url (no Authorization header).
curl -X PUT --upload-file interview.m4a -H "Content-Type: audio/mp4" "$UPLOAD_URL"

# 3. Start transcribing.
curl -X POST https://txscribe.ai/api/v1/transcriptions/$ID/start \
  -H "Authorization: Bearer $TXSCRIBE_API_KEY"

# 4. When status is "done" (or your webhook says so), fetch the transcript.
curl "https://txscribe.ai/api/v1/transcriptions/$ID/transcript?format=srt" \
  -H "Authorization: Bearer $TXSCRIBE_API_KEY" -o interview.srt

Node.js (18+)

const API = "https://txscribe.ai/api/v1";
const headers = { Authorization: \`Bearer ${process.env.TXSCRIBE_API_KEY}\`, "Content-Type": "application/json" };

async function call(path, init = {}) {
  const response = await fetch(API + path, { ...init, headers: { ...headers, ...init.headers } });
  if (!response.ok) {
    const { error } = await response.json();
    throw new Error(\`${error.code}: ${error.message} (request ${error.requestId})\`);
  }
  return response;
}

const { transcription } = await (
  await call("/transcriptions", {
    method: "POST",
    headers: { "Idempotency-Key": crypto.randomUUID() },
    body: JSON.stringify({ url: "https://www.youtube.com/watch?v=VIDEO_ID" }),
  })
).json();

// Prefer a webhook; polling works too.
let current = transcription;
while (!["done", "error"].includes(current.status)) {
  await new Promise((resolve) => setTimeout(resolve, 15000));
  current = await (await call(\`/transcriptions/${transcription.id}\`)).json();
}
if (current.status === "done") {
  console.log(await (await call(\`/transcriptions/${current.id}/transcript?format=txt\`)).text());
}

Python (requests)

import os, time, uuid, requests

API = "https://txscribe.ai/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TXSCRIBE_API_KEY']}"

created = session.post(
    f"{API}/transcriptions",
    json={"url": "https://www.youtube.com/watch?v=VIDEO_ID"},
    headers={"Idempotency-Key": str(uuid.uuid4())},
)
created.raise_for_status()
transcription = created.json()["transcription"]

while transcription["status"] not in ("done", "error"):
    time.sleep(15)
    transcription = session.get(f"{API}/transcriptions/{transcription['id']}").json()

if transcription["status"] == "done":
    print(session.get(f"{API}/transcriptions/{transcription['id']}/transcript", params={"format": "txt"}).text)

Endpoints

POST /transcriptions

Create one from a url, or for a file you will upload (fileName, size).

GET /transcriptions

Your transcriptions, newest first. limit, cursor, status.

GET /transcriptions/{id}

Status and metadata.

POST /transcriptions/{id}/start

Start after the upload has landed, or try again after an error.

GET /transcriptions/{id}/transcript

?format= json, txt, docx, pdf, srt, vtt, fcpxml, premiere, nvivo, maxqda, atlasti. JSON has segments, words and speakers with their times; the others are the files the app's Export menu makes. from and to (seconds) keep only the paragraphs around a moment.

GET /search

?q= a word or phrase: which recordings said it, and the second each place starts at. Reads what was said, not titles, in the finished ones among your newest 500 transcriptions.

GET /transcriptions/{id}/summary

The summary and its status.

POST /transcriptions/{id}/summary

Ask for a summary. Optional language, scene and names (roles: people as roles and speaker labels, no personal names; as-said; the default depends on the template).

DELETE /transcriptions/{id}

Delete it, its media and everything made from it.

Uploading a file is three calls. Create the transcription with its fileName, contentType and size; the answer has an upload.url. PUT the whole file there, with no Authorization header — the URL is the permission, for that one file. Then POST /start. A link needs only the first call.

Options are the app's: language (auto by default, or one of the full codes the OpenAPI document lists, such as en-US; a bare en is refused), diarization (speaker labels, on by default), and for YouTube links captions to use the video's own captions when they fit. Your account's vocabulary list is applied to every transcription, as in the app.

Errors, limits and retries

Every error has the same shape: {"error": {"code", "message", "requestId"}}, sometimes with details. Branch on code; the message is for people. Every answer carries X-Request-Id — quote it to support@txscribe.ai.

402 quota_exceeded means the account is out of minutes for this period, exactly as in the app. Media longer than your plan allows ends as file_too_long: at once for a link whose length is known, and as the transcription's error.code once an uploaded file has been measured. A transcription can also end with error.code quota_exceeded after it was started, when the minutes ran out before it was charged — several started at once, and the others used the last of them. POST /start again once there are minutes; it is charged then.

Each key may make 120 requests a minute. Past that the answer is 429 rate_limited with Retry-After; X-RateLimit-Remaining says how close you are. The app's own limits apply too — on how many summaries an hour, and how many links can be checked in a few minutes.

POSTs take an Idempotency-Key header. Retrying with the same key and body within 24 hours returns the first success instead of creating a second transcription; the replay is marked Idempotent-Replayed: true. Refusals are not remembered, so a retry after you fix the cause runs again.

Webhooks

Register up to 5 https endpoints under Plan and billing → Developer API, each for any of transcription.completed, transcription.failed and summary.completed. Each event is a JSON POST with the transcription (and, for a summary, the summary) as the API returns them. “Send test event” sends webhook.test.

Answer with any 2xx within 10 seconds, and do the work afterwards. Anything else — a timeout, a 5xx, a redirect, which is never followed — is tried again 5 times, after 0.5 min, 2 min, 10 min, 1 h, 6 h. The same event can arrive twice; its id stays the same, so deduplicate on it. The last 50 deliveries and what your endpoint answered are listed in your settings.

Webhooks are only sent to public internet addresses. An address that resolves to a private, loopback, link-local or cloud metadata address is refused when you add it, and checked again at every send.

Every delivery is signed with the endpoint's secret (shown once, when you add it): Txscribe-Signature: t=1790000000,v1=…, where v1 is the hex HMAC-SHA256 of {t}.{raw body}. Check it against the body exactly as it arrived, before parsing it, and refuse a t more than 5 minutes from your clock.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

// header: the Txscribe-Signature header. rawBody: the request body exactly as received, as a string.
export function verifyTxscribeSignature(secret, header, rawBody, toleranceSeconds = 300) {
  let timestamp = NaN;
  const signatures = [];
  for (const part of header.split(",")) {
    const [name, value] = part.trim().split("=", 2);
    if (name === "t") timestamp = Number(value);
    if (name === "v1" && value) signatures.push(value);
  }
  if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(\`${timestamp}.${rawBody}\`).digest();
  return signatures.some((signature) => {
    const given = Buffer.from(signature, "hex");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

Python

import hashlib, hmac, time

# header: the Txscribe-Signature header. raw_body: the request body exactly as received, as bytes.
def verify_txscribe_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    timestamp, signatures = None, []
    for part in header.split(","):
        name, _, value = part.strip().partition("=")
        if name == "t":
            timestamp = value
        elif name == "v1":
            signatures.append(value)
    if timestamp is None or not timestamp.isdigit() or abs(time.time() - int(timestamp)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, signature) for signature in signatures)

MCP: Claude, ChatGPT and other AI apps

The API is also an MCP server, so an AI assistant can transcribe a link and read the transcript and summary back as you. It has 8 tools. 5 only read: list_transcriptions, search_transcriptions, get_transcription, get_transcript and get_summary. 3 change your account: transcribe_link, request_summary and delete_transcription — a new transcription uses your minutes, and a deletion cannot be undone. There is no upload tool; files go through the app or the API.

Hosted at https://txscribe.ai/mcp (Streamable HTTP). In Claude or ChatGPT, add it as a custom connector with that URL: you sign in to txscribe and choose Allow. The connection is an API key the app holds for you, listed under Plan and billing → Developer API, where Revoke disconnects it. Clients that take headers can send Authorization: Bearer txs_live_… instead.

Local: the npm package @txscribe/mcp runs the same tools over stdio, with your key in TXSCRIBE_API_KEY.

Every MCP request counts against the key's 120 a minute, and a tool call counts once more for the API call it makes. Errors come back as tool errors carrying the API's code and request id.

Claude Code — the hosted server, signing in with OAuth

claude mcp add --transport http txscribe https://txscribe.ai/mcp

Claude Code with an API key instead (other clients: the same URL and header)

claude mcp add --transport http txscribe https://txscribe.ai/mcp \
  --header "Authorization: Bearer $TXSCRIBE_API_KEY"

Claude Desktop, Cursor and others that run a command — the npm package

{
  "mcpServers": {
    "txscribe": {
      "command": "npx",
      "args": ["-y", "@txscribe/mcp"],
      "env": { "TXSCRIBE_API_KEY": "txs_live_…" }
    }
  }
}