API and MCP documentation
Transcribe audio from your own code or from an AI assistant. A REST API (versioned under /api/v1) and an MCP server share the same key, limits and balance. Included with the Pro and Studio plans.
Quickstart
Create a key in My account, then create a job, poll it and read the result:
export TLTL_API_KEY=tltl_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 1. create the job
curl -X POST https://toolongtolisten.com/api/v1/transcriptions \
-H "Authorization: Bearer $TLTL_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/meeting.mp3","language":"auto","name":"Weekly meeting"}'
# 2. poll until "status" is "completed"
curl https://toolongtolisten.com/api/v1/transcriptions/$ID -H "Authorization: Bearer $TLTL_API_KEY"
# 3. read the result: json | text | srt | vtt
curl "https://toolongtolisten.com/api/v1/transcriptions/$ID/transcript?format=srt" -H "Authorization: Bearer $TLTL_API_KEY"const BASE = 'https://toolongtolisten.com';
const headers = { Authorization: `Bearer ${process.env.TLTL_API_KEY}`, 'Content-Type': 'application/json' };
const created = await fetch(`${BASE}/api/v1/transcriptions`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({ url: 'https://example.com/meeting.mp3', language: 'auto' }),
}).then((r) => r.json());
let job = created;
while (job.status === 'queued' || job.status === 'processing') {
await new Promise((r) => setTimeout(r, 5000));
job = await fetch(`${BASE}/api/v1/transcriptions/${created.id}`, { headers }).then((r) => r.json());
}
if (job.status !== 'completed') throw new Error(job.error?.message ?? job.status);
const transcript = await fetch(`${BASE}/api/v1/transcriptions/${created.id}/transcript`, { headers }).then((r) => r.json());
console.log(transcript.text, transcript.segments.length);import os, time, uuid, requests
BASE = "https://toolongtolisten.com"
H = {"Authorization": f"Bearer {os.environ['TLTL_API_KEY']}"}
job = requests.post(f"{BASE}/api/v1/transcriptions", headers={**H, "Idempotency-Key": str(uuid.uuid4())},
json={"url": "https://example.com/meeting.mp3", "language": "auto"}).json()
while job["status"] in ("queued", "processing"):
time.sleep(5)
job = requests.get(f"{BASE}/api/v1/transcriptions/{job['id']}", headers=H).json()
if job["status"] != "completed":
raise RuntimeError(job.get("error") or job["status"])
transcript = requests.get(f"{BASE}/api/v1/transcriptions/{job['id']}/transcript", headers=H).json()
print(transcript["text"])Authentication
Send Authorization: Bearer <key> (the X-API-Key header also works). Keys look like tltl_<id>_<secret>, are shown once when created and only a hash is stored. You can have up to 5 active keys and revoke any of them at any time in My account. A revoked or unknown key returns 401 invalid_api_key; an account without Pro or Studio returns 403 plan_required.
Create a transcription
From a public https URL (a direct media file up to 100 MB, downloaded by our server) or by uploading the file. Formats: mp3, m4a, ogg, opus, wav, webm, flac and mp4. YouTube and Google Drive page links are not supported, only direct media URLs. The response is immediate (202) and the job runs in a queue. Optional fields for every creation call: name, language (an ISO code such as en, or auto), word_timestamps, metadata (up to 10 string pairs echoed back) and webhook_url.
Upload a local file
Small files (up to 30 MB) can be sent as multipart to the same endpoint. For anything up to 100 MB, ask for a signed upload URL, PUT the file straight to storage and then start the job. With "resumable": true the URL is a resumable upload session instead.
# 1. reserve a slot (file_name decides the format)
curl -X POST https://toolongtolisten.com/api/v1/uploads \
-H "Authorization: Bearer $TLTL_API_KEY" -H "Content-Type: application/json" \
-d '{"file_name":"meeting.m4a","content_type":"audio/mp4","language":"auto"}'
# -> { "id": "...", "upload": { "method": "PUT", "url": "https://storage.googleapis.com/...", "headers": {...} }, "complete_url": "..." }
# 2. PUT the file to upload.url with exactly the returned headers
curl -X PUT "$UPLOAD_URL" -H "Content-Type: audio/mp4" -H "x-goog-content-length-range: 0,104857600" --data-binary @meeting.m4a
# 3. start the job, then poll as usual
curl -X POST https://toolongtolisten.com/api/v1/uploads/$ID/complete -H "Authorization: Bearer $TLTL_API_KEY"
# small files (up to 30 MB) can go straight to the create endpoint
curl -X POST https://toolongtolisten.com/api/v1/transcriptions -H "Authorization: Bearer $TLTL_API_KEY" -F "file=@meeting.m4a" -F "name=Weekly meeting"Status and result
Status is awaiting_upload, queued, processing, completed, failed or canceled. A completed transcript comes as JSON (text, language, timed segments, speakers), plain text, SRT or VTT. Add words=1 for word timings (jobs created with word_timestamps), speakers=0 to leave speaker names out and lang=en for a translation made in the web app. Other calls: list (GET /api/v1/transcriptions, with cursor and status), DELETE a job and its audio, POST .../cancel, POST .../summary (modes short, topics, minutes, chapters; optional language such as pt-BR or en sets the output language, default is the language detected in the transcript) and GET /api/v1/usage.
Webhooks
With webhook_url (https only, public hosts), we POST a JSON event, transcription.completed or transcription.failed, up to 3 attempts. The body carries transcript_url, not the text. Headers: X-TLTL-Signature: t=<unix>,v1=<hmac> where the HMAC is SHA-256 of "<t>.<raw body>" with the webhook secret shown when the key was created, X-TLTL-Event and X-TLTL-Delivery (stable across retries, use it to deduplicate). Verify against the raw body and reject timestamps older than 5 minutes. To resend: POST /api/v1/transcriptions/{id}/webhook.
{
"event": "transcription.completed",
"id": "9fKd3mQ1xZ",
"status": "completed",
"language": "en",
"duration_seconds": 312.4,
"error": null,
"metadata": { "task": "ST-123" },
"transcript_url": "https://toolongtolisten.com/api/v1/transcriptions/9fKd3mQ1xZ/transcript",
"created_at": "2026-10-22T18:30:00.000Z"
}import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(secret, rawBody, header) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac('sha256', secret).update(parts.t + '.' + rawBody).digest();
const given = Buffer.from(parts.v1 || '', 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Idempotency
Send an Idempotency-Key header (up to 255 characters) on POST /api/v1/transcriptions and POST /api/v1/uploads. A retry with the same key and the same request returns the original response with Idempotent-Replayed: true instead of creating a second job; the same key with a different request is a 422 idempotency_key_reused. Keys live for 24 hours.
Limits and quotas
- Per key: 60 requests per minute, 60 new transcriptions per hour, 3000 per month and 30 summaries per hour. Every response has
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset; when exceeded the response is 429 withRetry-After(rate_limitedorquota_exceeded). - Usage is billed in seconds of audio from the same balance as the web app. With no balance the answer is 402
insufficient_credits.GET /api/v1/usageshows limits, usage and the balance. - Audio is kept according to the plan retention rules; transcripts stay until you delete them.
Tenants (many organizations, one key)
If your product serves several organizations with a single key, tell us which one each call is for. Send X-Tenant-Id (required to be attributed: 1 to 64 characters, letters, digits, _ . : @ -, starting with a letter or digit) and optionally X-Tenant-Name (display name, up to 80 characters, percent encoding accepted) on any REST call and on the MCP endpoint. MCP tools that create jobs also accept tenant_id and tenant_name arguments, which win over the headers. A malformed id is a 400. Calls without the header are attributed to the key alone.
The tenant is stored on the job and returned as tenant: { id, name } on every transcription and in the webhook body. Requests, errors, latency, webhooks, audio minutes and estimated cost are reported per key and per tenant to the account operator.
A key can carry a monthly minutes limit per tenant (a default for tenants without their own entry, and overrides; a tenant can be unlimited and still tracked). When a tenant has used its minutes in the current UTC month, creating a job answers 402 tenant_limit_reached with Retry-After set to the start of next month. Minutes count finished audio plus audio still in the queue. GET /api/v1/usage?tenant=<id> (or the header) returns tenant.monthly_minutes_limit, minutes_used, minutes_in_progress, minutes_remaining, limit_reached and resets_at.
curl -X POST https://toolongtolisten.com/api/v1/transcriptions \
-H "Authorization: Bearer $TLTL_API_KEY" \
-H "X-Tenant-Id: org_acme" \
-H "X-Tenant-Name: Acme Ltda" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/meeting.m4a"}'
# 402 when org_acme used its minutes this month
# {"error": {"code": "tenant_limit_reached", "status": 402, "message": "...", "request_id": "req_..."}}
curl "https://toolongtolisten.com/api/v1/usage?tenant=org_acme" -H "Authorization: Bearer $TLTL_API_KEY"Errors
Every error has the same shape and an X-Request-Id header. Branch on code, never on the message. Include the request_id when you contact support.
HTTP/1.1 429 Too Many Requests
Retry-After: 21
X-Request-Id: req_Qm3b0u9vTq1k
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
{ "error": { "code": "rate_limited", "status": 429, "message": "Rate limit exceeded. Retry later.", "request_id": "req_Qm3b0u9vTq1k" } }Codes
invalid_requestinvalid_api_keyplan_requiredforbiddennot_foundconflictnot_finishedstill_processingupload_missinginsufficient_creditspayload_too_largeunsupported_media_typedownload_failedrate_limitedquota_exceededtenant_limit_reachedidempotency_key_reusedidempotency_in_progressservice_unavailableinternal_error
MCP server
The same backend speaks the Model Context Protocol over Streamable HTTP at https://toolongtolisten.com/api/mcp. It is stateless, authenticated with the same API key, and supports the 2026-07-28 revision as well as 2025-11-25, 2025-06-18 and 2025-03-26 clients. Add it to Claude Code:
claude mcp add --transport http tltl https://toolongtolisten.com/api/mcp \
--header "Authorization: Bearer $TLTL_API_KEY"Other clients that take a JSON configuration (Cursor, Claude Desktop through mcp-remote, and so on):
{
"mcpServers": {
"tltl": {
"url": "https://toolongtolisten.com/api/mcp",
"headers": { "Authorization": "Bearer tltl_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
}
}
}
// Claude Desktop (stdio bridge)
{
"mcpServers": {
"tltl": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://toolongtolisten.com/api/mcp", "--header", "Authorization: Bearer ${TLTL_API_KEY}"],
"env": { "TLTL_API_KEY": "tltl_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
}
}
}Tools
transcribe_urlStart a transcription from a public media URL.transcribe_uploadGet a signed upload URL for a local file; PUT the file, then call complete_upload.complete_uploadStart the job after the upload.get_transcriptionStatus, and the transcript text or timed segments when completed.list_transcriptionsList jobs with status and paging.export_subtitlesSRT or VTT text of a completed job.summarizeShort summary, topics, meeting minutes or chapters.cancel_transcription / delete_transcriptionStop a queued job or remove a finished one.
Local files cannot be read by a remote server, so for a file on your machine the assistant calls transcribe_upload, uploads it (for example with curl) and then calls complete_upload.
npx @modelcontextprotocol/inspector --cli https://toolongtolisten.com/api/mcp --transport http \
--header "Authorization: Bearer $TLTL_API_KEY" --method tools/listOpenAPI
The OpenAPI 3.1 description of the REST API is at /api/v1/openapi.json. Import it into Postman, Insomnia or a code generator.