API documentation
The Techtuel API turns an audio or video file into text. Two calls are enough: you submit a URL, then you retrieve the transcript. Everything is JSON, over HTTPS, with key-based authentication.
- Base URL
- https://api.techtuel.com/v1
- Format
- JSON · UTF-8
- Authentication
- API key (Bearer)
Authentication
Every request must carry your API key in the Authorization header. Keys are prefixed with txl_. Keep them secret: a key grants access to your quota.
Authorization: Bearer txl_your_secret_keyQuickstart
One call. Send a source — a public URL (YouTube, podcast, MP3) or a file you uploaded via POST /uploads — and get the transcript back in the same response. No job id, no polling loop.
curl -X POST https://api.techtuel.com/v1/transcribe \
-H "Authorization: Bearer txl_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "source_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }'
{ "status": "completed", "transcript": "..." }Long audio does not fit in one HTTP request. If the transcription is still running after 30 seconds (raise it with ?wait=, up to 120), you get 202 and a job id instead — the job keeps running, and GET /transcriptions/{id} retrieves it when it is done. Add ?format=srt there to download subtitles. Every endpoint is detailed below.
API reference
Every public endpoint, generated from the API schema — so it always matches what the service actually serves.
Transcriptions
Submit audio for transcription and read the result back.
Transcribe and wait for the result
Submit a source and receive the finished transcript in the SAME response — no job id, no polling loop. Returns **200** with `transcript` (and `segments`) once the transcription completes, which is immediate when the source is already in the shared cache. Long audio does not fit in one HTTP request. When the transcription is still running after the wait window (default 30s, `?wait=` up to 120s), the response is **202** with the job `id` and `status: "processing"` — the job keeps running, and GET /transcriptions/{id} retrieves it exactly as with the asynchronous API. Treat 202 as "not finished yet", never as a failure. A job that fails within the window returns **200** with `status: "failed"` and `error` set: the request succeeded, the transcription did not.
Query parameters
| Field | Type | Description |
|---|---|---|
| wait | integer | Seconds to wait before falling back to 202 (default 30, max 120) |
Body
| Field | Type | Description |
|---|---|---|
| audio_url | string | |
| preferred_language | string | PreferredLanguage is the ISO 639-1 code the caller wants the transcript in. Omit it to get the language actually spoken in the audio. It is a PREFERENCE, not a guarantee: when the transcript cannot be translated the original is returned rather than an error, and the response's `language` field always states what was really produced. |
| source_url | string | |
| upload_id | string |
curl -X POST https://api.techtuel.com/v1/transcribe \
-H "Authorization: Bearer txl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"audio_url": "https://example.com/episode.mp3",
"preferred_language": "fr",
"source_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"upload_id": "up_abc123"
}'{
"api_key_id": "key_abc123",
"created_at": "2026-07-21T09:30:00Z",
"detected_language": "it",
"error": "string",
"id": "job_abc123",
"language": "fr",
"minutes": 12.5,
"segments": [
{
"start_seconds": 12.5,
"text": "Bonjour et bienvenue dans cet"
}
],
"source_kind": "url",
"source_url": "https://youtu.be/dQw4w9WgXcQ",
"status": "processing",
"title": "Le 6-9 de France Inter",
"transcript": "string",
"translated": true
}- 400
- Bad Request
- 401
- Unauthorized
- 402
- Refused: `reason` says which — `quota_exceeded` or `payment_failed`
Transcribe a local file in one call
Send the audio/video file itself and receive the finished transcript in the SAME response — no upload slot, no polling loop. This is the one-call form of the three-step upload flow (POST /uploads → PUT → POST /transcribe). Send `multipart/form-data` with the bytes in a `file` part; `Content-Type` of that part decides the accepted format. Limited to 100 MiB per request, well below what POST /uploads accepts (200 MiB audio, 1 GiB video): here the bytes travel through the API, whereas a presigned upload goes straight to storage. A larger file gets **413** naming the presigned flow — use it, it is not a downgrade but the shape that works for big files. Wait semantics are identical to POST /transcribe: **200** once the transcription completes, **202** with the job `id` when it is still running after the wait window (default 30s, `?wait=` up to 120s). Upload time counts against that window, so a large file is likelier to answer 202 — the job keeps running and GET /transcriptions/{id} retrieves it.
Query parameters
| Field | Type | Description |
|---|---|---|
| wait | integer | Seconds to wait before falling back to 202 (default 30, max 120) |
curl -X POST https://api.techtuel.com/v1/transcribe/file \
-H "Authorization: Bearer txl_your_api_key" \
-F file=@/path/to/episode.mp3 \
-F preferred_language=fr{
"api_key_id": "key_abc123",
"created_at": "2026-07-21T09:30:00Z",
"detected_language": "it",
"error": "string",
"id": "job_abc123",
"language": "fr",
"minutes": 12.5,
"segments": [
{
"start_seconds": 12.5,
"text": "Bonjour et bienvenue dans cet"
}
],
"source_kind": "url",
"source_url": "https://youtu.be/dQw4w9WgXcQ",
"status": "processing",
"title": "Le 6-9 de France Inter",
"transcript": "string",
"translated": true
}- 400
- Missing file part or unsupported format
- 401
- Unauthorized
- 402
- Refused: `reason` says which — `quota_exceeded` or `payment_failed`
- 413
- File exceeds the inline limit — use POST /uploads
- 503
- Uploads not configured
List transcription jobs
Return the authenticated owner's transcription jobs, newest first, one page at a time. Use ?page= (1-based) and ?per_page= (default 20, max 100); the response carries `total` and `has_more` so a client can append the next page. Rows do NOT include the joined transcript — only `preview_segments` (the first few timed segments) and `has_transcript`. Fetch GET /transcriptions/{id} for the full text of a single job. Pass ?api_key_id= to list only jobs submitted by that API key (jobs created from the web console are excluded).
Query parameters
| Field | Type | Description |
|---|---|---|
| api_key_id | string | Restrict to jobs submitted by this API key id |
| page | integer | 1-based page number |
| per_page | integer | Jobs per page (1-100) |
curl -X GET https://api.techtuel.com/v1/transcriptions \
-H "Authorization: Bearer txl_your_api_key"{
"has_more": true,
"jobs": [
{
"api_key_id": "key_abc123",
"created_at": "2026-07-21T09:30:00Z",
"error": "string",
"has_transcript": true,
"id": "job_abc123",
"minutes": 12.5,
"preview_segments": [],
"source_kind": "url",
"source_url": "https://youtu.be/dQw4w9WgXcQ",
"status": "processing",
"title": "Le 6-9 de France Inter"
}
],
"page": 1,
"per_page": 10,
"total": 47
}- 401
- Unauthorized
Create a transcription job
Submit a source for transcription. Returns immediately with a job; poll GET /transcriptions/{id} until `status` is `completed` or `failed`. Prefer POST /transcribe when you just want the text: it returns the finished transcript in one request and needs no polling loop. `status` is one of `processing`, `claimed` (a worker took the job), `completed` or `failed`. Only the last two are terminal — stop polling on either. A cached or already-submitted source comes back `completed` right away, with the transcript in the same response.
source_url for a URL the worker resolves (direct audio/video, podcast page or YouTube), or upload_id for a file sent via POST /uploads. audio_url is a legacy alias for source_url; prefer source_url in new code. If both a URL and an upload_id are sent, upload_id wins.Body
| Field | Type | Description |
|---|---|---|
| audio_urlone of | string | |
| preferred_language | string | PreferredLanguage is the ISO 639-1 code the caller wants the transcript in. Omit it to get the language actually spoken in the audio. It is a PREFERENCE, not a guarantee: when the transcript cannot be translated the original is returned rather than an error, and the response's `language` field always states what was really produced. |
| source_urlone of | string | |
| upload_idone of | string |
Exactly one source is required
curl -X POST https://api.techtuel.com/v1/transcriptions \
-H "Authorization: Bearer txl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"source_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"preferred_language": "fr"
}'{
"api_key_id": "key_abc123",
"created_at": "2026-07-21T09:30:00Z",
"detected_language": "it",
"error": "string",
"id": "job_abc123",
"language": "fr",
"minutes": 12.5,
"segments": [
{
"start_seconds": 12.5,
"text": "Bonjour et bienvenue dans cet"
}
],
"source_kind": "url",
"source_url": "https://youtu.be/dQw4w9WgXcQ",
"status": "processing",
"title": "Le 6-9 de France Inter",
"transcript": "string",
"translated": true
}- 400
- Bad Request
- 401
- Unauthorized
- 402
- Refused: `reason` says which — `quota_exceeded` (the period's allowance is spent; it refills on renewal) or `payment_failed` (the subscription is unpaid; update the payment method)
Get a transcription job
Fetch a transcription job by id. The default response is the JSON job envelope (status + joined text). Pass ?format= to download the transcript in a timed format once the job is completed: `txt` (plain text), `json` (text + timed segments), `srt` (SubRip subtitles), `vtt` (WebVTT subtitles).
Path parameters
| Field | Type | Description |
|---|---|---|
| idrequired | string | Job id |
Query parameters
| Field | Type | Description |
|---|---|---|
| format | string | Output formattxt · json · srt · vtt |
curl -X GET https://api.techtuel.com/v1/transcriptions/job_abc123 \
-H "Authorization: Bearer txl_your_api_key"{
"api_key_id": "key_abc123",
"created_at": "2026-07-21T09:30:00Z",
"detected_language": "it",
"error": "string",
"id": "job_abc123",
"language": "fr",
"minutes": 12.5,
"segments": [
{
"start_seconds": 12.5,
"text": "Bonjour et bienvenue dans cet"
}
],
"source_kind": "url",
"source_url": "https://youtu.be/dQw4w9WgXcQ",
"status": "processing",
"title": "Le 6-9 de France Inter",
"transcript": "string",
"translated": true
}- 401
- Unauthorized
- 404
- Not Found
- 409
- Conflict
- 422
- Unprocessable Entity
Delete a transcription
Removes a finished (completed or failed) transcription from the caller's history. The transcript itself stays in the shared source cache, so re-submitting the same source later is still served without re-transcribing. A job still in flight cannot be deleted; wait for it to finish.
Path parameters
| Field | Type | Description |
|---|---|---|
| idrequired | string | Job id |
curl -X DELETE https://api.techtuel.com/v1/transcriptions/job_abc123 \
-H "Authorization: Bearer txl_your_api_key""string"- 404
- Not Found
Retry a failed transcription
Re-queues a failed transcription on the SAME job, so the history keeps one row per source instead of accumulating one per attempt. Only failed jobs can be retried, and the retry is subject to the monthly quota.
Path parameters
| Field | Type | Description |
|---|---|---|
| idrequired | string | Job id |
curl -X POST https://api.techtuel.com/v1/transcriptions/job_abc123/retry \
-H "Authorization: Bearer txl_your_api_key"{
"api_key_id": "key_abc123",
"created_at": "2026-07-21T09:30:00Z",
"detected_language": "it",
"error": "string",
"id": "job_abc123",
"language": "fr",
"minutes": 12.5,
"segments": [
{
"start_seconds": 12.5,
"text": "Bonjour et bienvenue dans cet"
}
],
"source_kind": "url",
"source_url": "https://youtu.be/dQw4w9WgXcQ",
"status": "processing",
"title": "Le 6-9 de France Inter",
"transcript": "string",
"translated": true
}- 402
- Refused: `reason` is `quota_exceeded` or `payment_failed`
- 404
- Not Found
Web extraction
Turn any public web page into clean, structured text: title, metadata, and the readable content without navigation or ads.
Extract clean, structured text from a web page
Fetches a public web page, strips navigation/ads/boilerplate, and returns its title, metadata and readable text — both flattened and split into positioned segments. Synchronous: no job id, no polling. Costs 1 credit; a page already in the global cache is free.
Body
| Field | Type | Description |
|---|---|---|
| url | string |
curl -X POST https://api.techtuel.com/v1/v1/extract \
-H "Authorization: Bearer txl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/article"
}'{
"author": "string",
"cached": false,
"charCount": 0,
"creditsCharged": 0,
"description": "string",
"extractedAt": "string",
"imageUrl": "string",
"segments": [
{
"charLength": 0,
"charOffset": 0,
"sectionTitle": "string",
"text": "string"
}
],
"siteName": "string",
"sourceId": "string",
"sourceType": "webpage",
"text": "string",
"title": "string",
"url": "string"
}- 400
- invalid or disallowed url
- 402
- monthly credit allowance exhausted
- 404
- page not found
- 422
- page yielded no readable content (paywall, JS-only app, or no prose)
- 502
- page could not be fetched
- 503
- site is rate limiting us; retry later
Uploads
Transcribe a local file: request a slot, PUT the bytes, then create a job from the returned id.
Create a file upload slot
Returns a presigned PUT URL. Upload the audio/video file directly to put_url with exactly the declared size_bytes, then POST /transcriptions with the returned upload_id. content_type and size_bytes are signed into the URL: the store rejects an upload that does not match. Size limits depend on the medium: 200 MiB for audio (the full 2h at up to 232 kbit/s), 1 GiB for video (about 30 min of 1080p at 4 Mbit/s). Longer video does not fit an upload — submit it by URL, where only the 2h duration cap applies. The URL is valid for 30 minutes.
PUT the raw bytes to the returned put_url with exactly the declared content_type and size_bytes (both are signed into the URL — a mismatch is rejected), then POST /transcriptions with the returned upload_id.Body
| Field | Type | Description |
|---|---|---|
| content_typerequired | string | |
| size_bytesrequired | integer | |
| filename | string |
curl -X POST https://api.techtuel.com/v1/uploads \
-H "Authorization: Bearer txl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"content_type": "audio/mpeg",
"filename": "episode.mp3",
"size_bytes": 18400000
}'{
"expires_at": "2026-06-04T12:15:00Z",
"put_url": "https://s3.fr-par.scw.cloud/...",
"upload_id": "up_abc123"
}- 400
- Missing/unsupported content_type or size_bytes
- 401
- Unauthorized
- 413
- File exceeds the size limit
- 503
- Uploads not configured
API keys
Manage the `txl_` keys that authenticate every call.
List API keys
List the authenticated user's API keys. The token hash and raw secret are never returned — only metadata (id, name, prefix, last 4 chars, timestamps).
curl -X GET https://api.techtuel.com/v1/keys \
-H "Authorization: Bearer txl_your_api_key"{
"keys": [
{
"created_at": "2026-06-05T10:00:00Z",
"id": "key_abc123",
"last4": "a1b2",
"last_used_at": "2026-06-05T12:30:00Z",
"name": "production server",
"prefix": "txl_"
}
]
}- 401
- Unauthorized
Create an API key
Mint a new API key for the authenticated user. The raw token is returned exactly once in the `token` field — store it now, it can never be retrieved again. Only available to a logged-in web session (an API key cannot mint another key).
Body
| Field | Type | Description |
|---|---|---|
| name | string |
curl -X POST https://api.techtuel.com/v1/keys \
-H "Authorization: Bearer txl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "production server"
}'{
"created_at": "2026-06-05T10:00:00Z",
"id": "key_abc123",
"last4": "a1b2",
"last_used_at": "2026-06-05T12:30:00Z",
"name": "production server",
"prefix": "txl_",
"token": "txl_a1b2c3d4e5f6..."
}- 400
- Bad Request
- 401
- Unauthorized
- 403
- Authenticated with an API key, not a session
Revoke an API key
Permanently revoke one of the authenticated user's API keys. Idempotent: revoking an unknown or already-revoked key still returns 204. Only available to a logged-in web session.
Path parameters
| Field | Type | Description |
|---|---|---|
| idrequired | string | Key id |
curl -X DELETE https://api.techtuel.com/v1/keys/key_abc123 \
-H "Authorization: Bearer txl_your_api_key"- 401
- Unauthorized
- 403
- Authenticated with an API key, not a session
Billing & usage
Pricing tiers, subscription lifecycle and the quota you have left.
Start a subscription / credit checkout
Returns a hosted-checkout URL for the requested plan. Redirect the user there to subscribe or buy credits.
Body
| Field | Type | Description |
|---|---|---|
| string | ||
| name | string | Name is the customer's display name, forwarded to the PSP so the customer record and its invoices are not anonymous. Optional: Lungor ignores it, Mollie uses it when creating the customer. |
| plan | string | |
| success_url | string |
curl -X POST https://api.techtuel.com/v1/billing/checkout \
-H "Authorization: Bearer txl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"email": "dev@example.com",
"name": "Ada Lovelace",
"plan": "pro",
"success_url": "https://app.example.com/billing/return"
}'{
"checkout_url": "string"
}- 400
- Bad Request
- 401
- Unauthorized
- 409
- Already subscribed
- 503
- Billing not configured
List the pricing tiers
Returns the public plans (Free, Pro, Max) with their monthly allocations and price, for the pricing page and the upgrade choice. A plan is only checkoutable when purchasable is true. No auth required.
curl -X GET https://api.techtuel.com/v1/billing/plans \
-H "Authorization: Bearer txl_your_api_key"{
"plans": [
{
"credits": 2000,
"currency": "EUR",
"name": "pro",
"price_cents": 900,
"purchasable": true,
"rate_per_min": 120
}
]
}Cancel your subscription
Stops future charges. Access continues until the end of the period already paid — the month is not refunded and not revoked. Idempotent: cancelling with no subscription succeeds.
curl -X DELETE https://api.techtuel.com/v1/billing/subscription \
-H "Authorization: Bearer txl_your_api_key"{
"access_until": "2026-08-24T09:00:00Z",
"active": true,
"status": "canceled"
}- 401
- Unauthorized
- 502
- Payment provider refused the cancellation
- 503
- Billing not configured
Move your subscription to another tier
Changes the tier of an active subscription. The new allowance applies immediately; the new price applies from the next renewal — the month already paid is neither re-charged nor refunded. Refused when the subscription is unpaid or cancelled.
Body
| Field | Type | Description |
|---|---|---|
| plan | string | Plan is the tier to move to, by name. |
curl -X POST https://api.techtuel.com/v1/billing/upgrade \
-H "Authorization: Bearer txl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"plan": "max"
}'{
"allowance_from": "now",
"charged_from": "2026-08-24T09:00:00Z",
"plan": "max"
}- 400
- Unknown or unsellable plan, or nothing to upgrade
- 401
- Unauthorized
- 409
- Subscription cannot be upgraded (unpaid or cancelled)
- 503
- Billing not configured
Get transcription usage / entitlement
Returns the caller's subscription status and monthly transcription consumption.
curl -X GET https://api.techtuel.com/v1/usage \
-H "Authorization: Bearer txl_your_api_key"{
"access_ends_at": "2026-08-03T00:00:00Z",
"active": true,
"billing_state": "healthy",
"credits_limit": 2000,
"credits_used": 84,
"plan": "pro",
"rate_per_min": 120,
"renews_at": "2026-08-18T00:00:00Z",
"status": "active"
}- 401
- Unauthorized
- 503
- Billing not configured
Account
Account-level operations.
Delete your own account
Cancels the caller's subscription at the payment provider and purges their transcript-api data (API keys, jobs, usage). Irreversible.
curl -X DELETE https://api.techtuel.com/v1/account \
-H "Authorization: Bearer txl_your_api_key"- 401
- Unauthorized
- 500
- Internal Server Error
- 502
- Subscription cancellation failed; nothing was deleted
Errors
Every error returns the same shape, with a readable message. No surprises: the HTTP code and the message say exactly what happened.
{ "message": "source_url or upload_id required" }| Code | Meaning |
|---|---|
| 400 | Invalid request. Neither source_url nor upload_id was provided, or the URL is malformed, not http(s), or points to a forbidden address. |
| 401 | Not authenticated. API key missing, malformed or revoked. |
| 402 | Not enough credits. Balance exhausted or monthly quota exceeded. Check GET /usage. |
| 403 | Forbidden. Operation restricted to a logged-in session: creating and revoking API keys cannot be done with an API key. |
| 404 | Not found. No job matches this identifier. |
| 409 | Job not finished. A timestamped format was requested for a transcription that is not completed yet. |
| 422 | Format unavailable. No timestamped segment exists for this transcript: srt and vtt are impossible, use txt or json. |
| 429 | Too many requests. Per-key rate limit exceeded — the cap depends on your plan (see Limits). The Retry-After header gives the wait time. |
| 500 | Internal error. Server-side incident. The request can be retried. |
| 503 | Service unavailable. A dependency is temporarily unreachable (billing, storage). Retry with a backoff. |
Limits
Going past one of these limits does not always return an HTTP error: a source that is too long is accepted, then the job ends as failed, with the reason in the error field and the reserved credits refunded in full.
| Maximum length per transcription | 120 minutes |
| Monthly quota (credits) | Free 100 · Pro 2,000 · Max 10,000 |
| Credit cost | 2 / audio minute · 1 per captioned video |
| Rate limit per API key | Free 12 req/min · Pro and Max 120 req/min |
| Maximum size of an uploaded audio file | 200 MiB — the full 2 h at up to 232 kbit/s |
| Maximum size of an uploaded video | 1 GiB — about 30 min of 1080p (4 Mbit/s) |
| Video longer than 30 min | By URL — only the maximum length applies |
| Upload URL validity | 30 minutes |
| Retention of an uploaded file | 7 days |
| Jobs returned by GET /transcriptions | 100 max |
| Uploadable audio formats | mp3 m4a wav ogg opus aac flac webm |
| Uploadable video formats | mp4 mpeg webm mov mkv |
| Unsupported sources | live streams |