REST · JSON · v1

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.

Header
Authorization: Bearer txl_your_secret_key

Quickstart

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.

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

POST/v1/transcribe

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.

Provide exactly one source: 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.

Query parameters

FieldTypeDescription
waitintegerSeconds to wait before falling back to 202 (default 30, max 120)

Body

FieldTypeDescription
audio_urlone ofstring
preferred_languagestringPreferredLanguage 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 ofstring
upload_idone ofstring

Exactly one source is required

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",
    "preferred_language": "fr"
  }'
Response · 200
{
  "api_key_id": "key_abc123",
  "created_at": "2026-07-21T09:30:00Z",
  "degraded_windows": 0,
  "detected_language": "it",
  "error": "source could not be resolved",
  "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": "completed",
  "title": "Le 6-9 de France Inter",
  "transcript": "Bonjour et bienvenue dans cet épisode…",
  "translated": true
}
400
Bad Request
401
Unauthorized
402
Refused: `reason` says which — `quota_exceeded`, `source_too_long` (longer than the plan's whole monthly allowance covers) or `payment_failed`
POST/v1/transcribe/file

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

FieldTypeDescription
waitintegerSeconds to wait before falling back to 202 (default 30, max 120)
Request
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
Response · 200
{
  "api_key_id": "key_abc123",
  "created_at": "2026-07-21T09:30:00Z",
  "degraded_windows": 0,
  "detected_language": "it",
  "error": "source could not be resolved",
  "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": "completed",
  "title": "Le 6-9 de France Inter",
  "transcript": "Bonjour et bienvenue dans cet épisode…",
  "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
GET/v1/transcriptions

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

FieldTypeDescription
api_key_idstringRestrict to jobs submitted by this API key id
pageinteger1-based page number
per_pageintegerJobs per page (1-100)
Request
curl -X GET https://api.techtuel.com/v1/transcriptions \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "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
POST/v1/transcriptions

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.

Provide exactly one source: 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

FieldTypeDescription
audio_urlone ofstring
preferred_languagestringPreferredLanguage 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 ofstring
upload_idone ofstring

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"
  }'
Response · 202
{
  "api_key_id": "key_abc123",
  "created_at": "2026-07-21T09:30:00Z",
  "degraded_windows": 0,
  "detected_language": "it",
  "error": "source could not be resolved",
  "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": "completed",
  "title": "Le 6-9 de France Inter",
  "transcript": "Bonjour et bienvenue dans cet épisode…",
  "translated": true
}
400
Bad Request
401
Unauthorized
402
Refused: `reason` says which — `quota_exceeded` (the period's allowance is spent; it refills on renewal), `source_too_long` (the source is longer than the plan's whole monthly allowance can cover; only a larger plan helps, waiting does not), or `payment_failed` (the subscription is unpaid; update the payment method)
GET/v1/transcriptions/{id}

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

FieldTypeDescription
idrequiredstringJob id

Query parameters

FieldTypeDescription
formatstringOutput formattxt · json · srt · vtt
Request
curl -X GET https://api.techtuel.com/v1/transcriptions/job_abc123 \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "api_key_id": "key_abc123",
  "created_at": "2026-07-21T09:30:00Z",
  "degraded_windows": 0,
  "detected_language": "it",
  "error": "source could not be resolved",
  "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": "completed",
  "title": "Le 6-9 de France Inter",
  "transcript": "Bonjour et bienvenue dans cet épisode…",
  "translated": true
}
401
Unauthorized
404
Not Found
409
Conflict
422
Unprocessable Entity
DELETE/v1/transcriptions/{id}

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

FieldTypeDescription
idrequiredstringJob id
Request
curl -X DELETE https://api.techtuel.com/v1/transcriptions/job_abc123 \
  -H "Authorization: Bearer txl_your_api_key"
Response · 204
"string"
404
Not Found
POST/v1/transcriptions/{id}/retry

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

FieldTypeDescription
idrequiredstringJob id
Request
curl -X POST https://api.techtuel.com/v1/transcriptions/job_abc123/retry \
  -H "Authorization: Bearer txl_your_api_key"
Response · 202
{
  "api_key_id": "key_abc123",
  "created_at": "2026-07-21T09:30:00Z",
  "degraded_windows": 0,
  "detected_language": "it",
  "error": "source could not be resolved",
  "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": "completed",
  "title": "Le 6-9 de France Inter",
  "transcript": "Bonjour et bienvenue dans cet épisode…",
  "translated": true
}
402
Refused: `reason` is `quota_exceeded`, `source_too_long` or `payment_failed`
404
Not Found

Translations

Translate a finished transcript into another language and read the translations back.

POST/v1/transcriptions/{id}/translate

Translate a transcript into another language

Renders a completed transcript into `language` (ISO 639-1), preserving timings. Cached per language, so asking twice is free. Translating into the transcript's own language returns it unchanged. Costs no extra credit today.

Path parameters

FieldTypeDescription
idrequiredstringJob id

Body

FieldTypeDescription
languagestring
Request
curl -X POST https://api.techtuel.com/v1/transcriptions/job_abc123/translate \
  -H "Authorization: Bearer txl_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "language": "fr"
  }'
Response · 200
{
  "degraded_windows": 0,
  "detected_language": "en",
  "language": "fr",
  "segments": [
    {
      "start_seconds": 12.5,
      "text": "Bonjour et bienvenue dans cet"
    }
  ],
  "source_id": "string",
  "transcript": "string"
}
400
language is required or malformed
404
transcript not found
409
transcript is not ready to translate
422
this transcript cannot be translated
503
translation backend is not configured
GET/v1/transcriptions/{id}/translations

List a transcript's available translations

Returns the languages already produced for this transcript, without their content.

Path parameters

FieldTypeDescription
idrequiredstringJob id
Request
curl -X GET https://api.techtuel.com/v1/transcriptions/job_abc123/translations \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "languages": [
    "fr",
    "es"
  ]
}
404
transcript not found
GET/v1/transcriptions/{id}/translations/{lang}

Read one translation of a transcript

Returns the transcript rendered into `lang` when it exists, else 404. Produce it first with POST /transcriptions/{id}/translate.

Path parameters

FieldTypeDescription
idrequiredstringJob id
langrequiredstringISO 639-1 language code
Request
curl -X GET https://api.techtuel.com/v1/transcriptions/job_abc123/translations/fr \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "degraded_windows": 0,
  "detected_language": "en",
  "language": "fr",
  "segments": [
    {
      "start_seconds": 12.5,
      "text": "Bonjour et bienvenue dans cet"
    }
  ],
  "source_id": "string",
  "transcript": "string"
}
404
transcript or translation not found

Web extraction

Turn any public web page into clean, structured text: title, metadata, and the readable content without navigation or ads.

POST/v1/extract

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: the text comes back in this response, with no polling. The extraction is also recorded under an `id`, so it is listed by GET /v1/transcriptions and can be re-read or deleted like a transcription. Costs 1 credit; a page already in the global cache is free.

Body

FieldTypeDescription
source_urlstring
urlstring
Request
curl -X POST https://api.techtuel.com/v1/extract \
  -H "Authorization: Bearer txl_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "source_url": "https://example.com/article",
    "url": "https://example.com/article"
  }'
Response · 200
{
  "author": "string",
  "cached": false,
  "char_count": 0,
  "credits_charged": 0,
  "description": "string",
  "extracted_at": "string",
  "id": "string",
  "image_url": "string",
  "segments": [
    {
      "char_length": 0,
      "char_offset": 0,
      "section_title": "string",
      "text": "string"
    }
  ],
  "site_name": "string",
  "source_id": "string",
  "source_type": "webpage",
  "text": "string",
  "title": "string",
  "url": "string"
}
400
invalid or disallowed url
402
Refused: `reason` says which — `quota_exceeded` or `payment_failed`
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.

POST/v1/uploads

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.

Three-step flow: call this endpoint, 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

FieldTypeDescription
content_typerequiredstring
size_bytesrequiredinteger
filenamestring
Request
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
  }'
Response · 201
{
  "expires_at": "2026-06-04T12:15:00Z",
  "put_url": "https://s3.fr-par.scw.cloud/...",
  "upload_id": "up_3f8c2a1e-9d41-4e7b-a12b-c45de6789012"
}
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.

GET/v1/keys

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

Request
curl -X GET https://api.techtuel.com/v1/keys \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "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
POST/v1/keys

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

The full key is returned once, at creation. It is stored hashed and cannot be read back — save it immediately.

Body

FieldTypeDescription
namestring
Request
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"
  }'
Response · 201
{
  "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
DELETE/v1/keys/{id}

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

FieldTypeDescription
idrequiredstringKey id
Request
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.

POST/v1/billing/checkout

Start a subscription / credit checkout

Returns a hosted-checkout URL for the requested plan. Redirect the user there to subscribe or buy credits.

Body

FieldTypeDescription
cancel_urlstringCancelURL is where a buyer who gives up on the provider's page is sent. Optional; usually the same page as success_url, which then reads the session to tell the two apart.
emailstring
namestringName is the customer's display name, forwarded to the provider so the customer record and its invoices are not anonymous. Optional.
payment_methodstringPaymentMethod is one of the ids GET /billing/checkout/methods returned for this plan. Omitted, the provider's own selection screen decides.
planstring
success_urlstring
Request
curl -X POST https://api.techtuel.com/v1/billing/checkout \
  -H "Authorization: Bearer txl_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "cancel_url": "https://app.example.com/billing/return",
    "email": "dev@example.com",
    "name": "Ada Lovelace",
    "payment_method": "card",
    "plan": "pro",
    "success_url": "https://app.example.com/billing/return"
  }'
Response · 200
{
  "checkout_url": "string"
}
400
Bad Request
401
Unauthorized
409
Already subscribed
503
Unavailable: `reason` says which — `billing_not_configured` (nothing wired, normal in dev) or `billing_unavailable` (configured and broken)
GET/v1/billing/checkout/{session_id}

Read how a checkout ended

Reports the state of a checkout the caller opened, for the page the provider returns to. The redirect itself carries no outcome — a refused card lands on the same URL as a paid one — so poll this while `status` is `pending` or `redirected`, and open access on `paid`. Someone else's session reads as not found.

Path parameters

FieldTypeDescription
session_idrequiredstringCheckout session id, from the `lungor_session_id` query parameter on the return URL
Request
curl -X GET https://api.techtuel.com/v1/billing/checkout/6f1c2a0e-9b7d-4c3e-8a5f-2d1e0b9c8a7f \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "failure_reason": "insufficient_funds",
  "paid": false,
  "session_id": "6f1c…",
  "status": "failed",
  "subscription_status": "incomplete"
}
401
Unauthorized
404
Not Found
503
Service Unavailable
GET/v1/billing/checkout/methods

List how a plan may be paid for

Returns the payment methods this plan accepts, in the order to offer them. Ask before showing a choice: a method absent here is one the checkout refuses.

Query parameters

FieldTypeDescription
planrequiredstringPlan name (pro, max)
Request
curl -X GET https://api.techtuel.com/v1/billing/checkout/methods?plan= \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "methods": [
    {
      "id": "card",
      "label": "Carte bancaire"
    }
  ]
}
400
Bad Request
401
Unauthorized
503
Unavailable: `reason` says which
POST/v1/billing/downgrade

Schedule a move to a smaller tier

Schedules a move DOWN to a smaller tier at the next renewal. Nothing changes today: the tier already paid for keeps its full allowance and rate limit until the end of the current period, and the smaller price is charged from the renewal that applies the smaller tier. Nothing is refunded and nothing is shortened. Refused when the target is larger than the current tier (use /billing/upgrade, which is immediate) or is the free tier (use DELETE /billing/subscription, which is a cancellation), and when the subscription is unpaid or already cancelled.

Body

FieldTypeDescription
planstringPlan is the smaller tier to move to, by name.
Request
curl -X POST https://api.techtuel.com/v1/billing/downgrade \
  -H "Authorization: Bearer txl_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "pro"
  }'
Response · 200
{
  "effective_at": "2026-08-24T09:00:00Z",
  "pending_plan": "pro",
  "plan": "max"
}
400
Unknown or unsellable plan, or nothing to downgrade
401
Unauthorized
409
Not a downgrade, or the subscription cannot be downgraded (unpaid or cancelled)
503
Unavailable: `reason` says which — `billing_not_configured` (nothing wired, normal in dev) or `billing_unavailable` (configured and broken)
DELETE/v1/billing/pending-change

Withdraw a scheduled tier change

Cancels a downgrade that was scheduled but has not taken effect yet. The tier currently held stays in force and keeps being charged at its own price. Free, and idempotent: withdrawing when nothing is scheduled succeeds.

Request
curl -X DELETE https://api.techtuel.com/v1/billing/pending-change \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "effective_at": "2026-08-24T09:00:00Z",
  "pending_plan": "pro",
  "plan": "max"
}
401
Unauthorized
502
Payment provider refused to restore the schedule
503
Unavailable: `reason` says which — `billing_not_configured` (nothing wired, normal in dev) or `billing_unavailable` (configured and broken)
GET/v1/billing/plans
No auth required

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.

Request
curl -X GET https://api.techtuel.com/v1/billing/plans \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "plans": [
    {
      "credits": 2000,
      "currency": "EUR",
      "name": "pro",
      "price_cents": 900,
      "purchasable": true,
      "rate_per_min": 120
    }
  ]
}
POST/v1/billing/repair

Restore the caller's free-tier subscription

Grants the free tier when Lungor holds no subscription for the caller, and reports whether it now does. Idempotent on the ledger: granting an owner Lungor already holds is a no-op there, so the button is safe to press twice. It deliberately does not skip the call for an owner recorded as granted locally — that record is exactly what a repair exists to disprove. Signup grants it best-effort, so an account created while Lungor was unreachable has no allowance and is refused every job. The balance reads repair it on their own, but only once the user hits one — this is the way out that does not depend on that.

Request
curl -X POST https://api.techtuel.com/v1/billing/repair \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "granted": false
}
401
Unauthorized
500
Internal Server Error
503
Service Unavailable
DELETE/v1/billing/subscription

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.

Request
curl -X DELETE https://api.techtuel.com/v1/billing/subscription \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "access_until": "2026-08-24T09:00:00Z",
  "active": true,
  "status": "canceled"
}
401
Unauthorized
502
Payment provider refused the cancellation
503
Unavailable: `reason` says which — `billing_not_configured` (nothing wired, normal in dev) or `billing_unavailable` (configured and broken)
POST/v1/billing/upgrade

Move your subscription to another tier

Changes the tier of an active subscription. Requires explicit consent (`consented`), and charges the prorated difference for the remainder of the current period on the card already on file — call POST /billing/upgrade/quote first to obtain that amount and show it. The charge happens BEFORE the tier is applied: a declined card answers 402 and leaves the subscription untouched. Below the collection floor nothing is charged and the tier is granted anyway. The new allowance applies immediately; the new price applies from the next renewal, and the period already paid is neither re-charged in full nor refunded.

Body

FieldTypeDescription
consent_versionstringConsentVersion identifies the wording the customer accepted, echoed back from the quote. The server re-renders that version rather than trusting any sentence from the client: the text IS the consent, so evidence of it must not be authored by the party it binds.
consentedbooleanConsented records that the customer was shown the amount and agreed to it. Required (FR2). An upgrade raises a recurring debit, and the checkout it mirrors already demands an explicit tick — the act that RAISES the amount must not be the one that asks for less. Absent or false is a 400, never a silent charge.
localestringLocale is which language the wording was displayed in ("fr" or "en"), so the archived sentence is the one that was actually on screen. Empty defaults to French — the authoritative text.
planstringPlan is the tier to move to, by name.
quoted_centsintegerQuotedCents is the amount the customer was shown, echoed back from the quote. It is what turns "they clicked" into "they agreed to THIS figure": a stale page quoting a larger period would otherwise consent to one amount and be charged another.
Request
curl -X POST https://api.techtuel.com/v1/billing/upgrade \
  -H "Authorization: Bearer txl_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "consent_version": "upgrade-v1",
    "consented": true,
    "locale": "fr",
    "plan": "max",
    "quoted_cents": 350
  }'
Response · 200
{
  "allowance_from": "now",
  "charged_from": "2026-08-24T09:00:00Z",
  "charged_now_cents": 350,
  "plan": "max"
}
400
Unknown or unsellable plan, nothing to upgrade, or consent missing
401
Unauthorized
402
The card was declined. Nothing changed: the tier is applied only once the payment lands
409
Subscription cannot be upgraded (unpaid or cancelled), or the plan is a downgrade — this endpoint only moves UP, since a smaller tier applied today would cut an allowance already paid for
503
Unavailable: `reason` says which — `billing_not_configured` (nothing wired, normal in dev) or `billing_unavailable` (configured and broken)
POST/v1/billing/upgrade/quote

Quote what an upgrade will cost today

Returns the amount charged immediately for moving to a larger tier (the price difference, prorated over the days left in the period already paid), and the recurring price from the next renewal. Charges nothing — call POST /billing/upgrade to commit. A charge of 0 means the proration fell under the collection floor and the tier is granted with no payment today.

Body

FieldTypeDescription
consent_versionstringConsentVersion identifies the wording the customer accepted, echoed back from the quote. The server re-renders that version rather than trusting any sentence from the client: the text IS the consent, so evidence of it must not be authored by the party it binds.
consentedbooleanConsented records that the customer was shown the amount and agreed to it. Required (FR2). An upgrade raises a recurring debit, and the checkout it mirrors already demands an explicit tick — the act that RAISES the amount must not be the one that asks for less. Absent or false is a 400, never a silent charge.
localestringLocale is which language the wording was displayed in ("fr" or "en"), so the archived sentence is the one that was actually on screen. Empty defaults to French — the authoritative text.
planstringPlan is the tier to move to, by name.
quoted_centsintegerQuotedCents is the amount the customer was shown, echoed back from the quote. It is what turns "they clicked" into "they agreed to THIS figure": a stale page quoting a larger period would otherwise consent to one amount and be charged another.
Request
curl -X POST https://api.techtuel.com/v1/billing/upgrade/quote \
  -H "Authorization: Bearer txl_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "consent_version": "upgrade-v1",
    "consented": true,
    "locale": "fr",
    "plan": "max",
    "quoted_cents": 350
  }'
Response · 200
{
  "charge_cents": 350,
  "charged_from": "2026-08-24T09:00:00Z",
  "consent_required": true,
  "consent_text": "J'accepte le prélèvement de 8 € aujourd'hui, puis 17 € par mois jusqu'à résiliation.",
  "consent_version": "upgrade-v1",
  "currency": "EUR",
  "pending_plan_cancelled": "pro",
  "plan": "max",
  "prorated_days": 15,
  "recurring_cents": 1200
}
400
Unknown or unsellable plan
401
Unauthorized
409
Subscription cannot be upgraded (unpaid or cancelled), or the plan is a downgrade — schedule it with /billing/downgrade
503
Service Unavailable
POST/v1/billing/webhook/lungor
No auth required

Lungor subscription webhook

Ingests Lungor's subscription lifecycle events. Authenticated by HMAC signature, not by API key. Not called by clients.

Request
curl -X POST https://api.techtuel.com/v1/billing/webhook/lungor \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
"string"
401
invalid signature
GET/v1/invoices

List the caller's invoices

Returns the authenticated customer's own invoices, newest first.

Request
curl -X GET https://api.techtuel.com/v1/invoices \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
[
  {
    "amount": 1900,
    "currency": "EUR",
    "customer_email": "client@example.com",
    "customer_name": "Client Test",
    "id": "3f8c2a1e-...",
    "issued_at": "2026-07-01T00:00:00Z",
    "number": "2026-0001",
    "status": "paid"
  }
]
401
Unauthorized
503
Invoicing is not configured
GET/v1/invoices/{id}

Get one of the caller's invoices

Path parameters

FieldTypeDescription
idrequiredstringInvoice ID
Request
curl -X GET https://api.techtuel.com/v1/invoices/3f8c2a1e-7b4d-4e2a-9c11-8a6f5d3b2c10 \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "currency": "EUR",
  "id": "string",
  "issued_at": "string",
  "lines": [
    {
      "amount": 1900,
      "description": "Abonnement Techtuel Pro",
      "kind": "flat",
      "quantity": 1,
      "unit": "credit",
      "unit_amount": 1900
    }
  ],
  "number": "2026-0001",
  "paid_at": "string",
  "pdf_url": "/v1/invoices/3f8c.../pdf",
  "period_end": "string",
  "period_start": "string",
  "status": "paid",
  "subtotal": 1900,
  "tax_amount": 0,
  "tax_rate": 0,
  "total": 1900
}
401
Unauthorized
404
Not Found
GET/v1/invoices/{id}/pdf

Download an invoice as a Factur-X PDF

Path parameters

FieldTypeDescription
idrequiredstringInvoice ID
Request
curl -X GET https://api.techtuel.com/v1/invoices/3f8c2a1e-7b4d-4e2a-9c11-8a6f5d3b2c10/pdf \
  -H "Authorization: Bearer txl_your_api_key"
401
Unauthorized
404
Not Found
GET/v1/usage

Get transcription usage / entitlement

Returns the caller's subscription status and monthly transcription consumption.

Request
curl -X GET https://api.techtuel.com/v1/usage \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "access_ends_at": "2026-08-03T00:00:00Z",
  "active": true,
  "billing_state": "healthy",
  "cancel_at_period_end": false,
  "credits_limit": 2000,
  "credits_used": 84,
  "currency": "EUR",
  "next_charge_cents": 1200,
  "pending_plan": "pro",
  "pending_plan_at": "2026-08-24T09:00:00Z",
  "period_start": "2026-07-03T00:00:00Z",
  "plan": "pro",
  "rate_per_min": 120,
  "renews_at": "2026-08-18T00:00:00Z",
  "status": "active"
}
401
Unauthorized
503
Unavailable: `reason` says which — `billing_not_configured` (nothing wired, normal in dev) or `billing_unavailable` (configured and broken)

Account

Account-level operations.

DELETE/v1/account

Delete your own account

Cancels the caller's subscription at the payment provider and purges their transcript-api data (API keys, jobs, usage). Irreversible.

Request
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
GET/v1/account/export
No auth required

Export all your transcriptions

Returns every transcription the account owns, in one JSON document, with the full text and timed segments of each. This is the GDPR portability export: unlike GET /transcriptions it is neither paginated nor projected — completeness is the point. Session-only: an export is the whole account history in one file, so a raw API key — which travels in scripts and CI — cannot trigger one.

Request
curl -X GET https://api.techtuel.com/v1/account/export \
  -H "Authorization: Bearer txl_your_api_key"
Response · 200
{
  "count": 57,
  "exported_at": "2026-07-31T09:30:00Z",
  "transcriptions": [
    {
      "created_at": "2026-07-21T09:30:00Z",
      "detected_language": "en",
      "error": "source could not be resolved",
      "id": "job_abc123",
      "language": "fr",
      "minutes": 12.5,
      "segments": [],
      "source_kind": "url",
      "source_url": "https://youtu.be/dQw4w9WgXcQ",
      "status": "completed",
      "title": "Le 6-9 de France Inter",
      "transcript": "Bonjour et bienvenue dans cet épisode…",
      "translated": true
    }
  ]
}
401
Unauthorized
500
Internal Server Error

Errors

Every error returns the same shape, with a readable message. No surprises: the HTTP code and the message say exactly what happened.

Error format
{ "message": "source_url or upload_id required" }
CodeMeaning
400Invalid request. Neither source_url nor upload_id was provided, or the URL is malformed, not http(s), or points to a forbidden address.
401Not authenticated. API key missing, malformed or revoked.
402Not enough credits. Balance exhausted or monthly quota exceeded. Check GET /usage.
403Forbidden. Operation restricted to a logged-in session: creating and revoking API keys cannot be done with an API key.
404Not found. No job matches this identifier.
409Job not finished. A timestamped format was requested for a transcription that is not completed yet.
422Format unavailable. No timestamped segment exists for this transcript: srt and vtt are impossible, use txt or json.
429Too many requests. Per-key rate limit exceeded — the cap depends on your plan (see Limits). The Retry-After header gives the wait time.
500Internal error. Server-side incident. The request can be retried.
503Service 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 transcription120 minutes
Monthly quota (credits)Free 100 · Pro 2,000 · Max 10,000
Credit cost2 / audio minute · 1 per captioned video
Rate limit per API keyFree 12 req/min · Pro and Max 120 req/min
Maximum size of an uploaded audio file200 MiB — the full 2 h at up to 232 kbit/s
Maximum size of an uploaded video1 GiB — about 30 min of 1080p (4 Mbit/s)
Video longer than 30 minBy URL — only the maximum length applies
Upload URL validity30 minutes
Retention of an uploaded file7 days
Jobs returned by GET /transcriptions100 max
Uploadable audio formatsmp3 m4a wav ogg opus aac flac webm
Uploadable video formatsmp4 mpeg webm mov mkv
Unsupported sourceslive streams