Overlay

API-Referenz

Transkribiere, verbessere und übersetze Untertitel programmatisch. Basis-URL, Authentifizierung, jeder Endpunkt und jeder Fehlercode — alles auf einer Seite.
Base URL: https://api.inwista.ai/v1

Authentifizierung

Erstelle einen API-Schlüssel unter Dashboard → API Keys (nur Workspace-Administratoren). Der Schlüssel wird nur einmal angezeigt — bewahre ihn wie ein Passwort auf.

Sende den Schlüssel bei jeder Anfrage als Bearer-Token. Schlüssel sind an genau einen Workspace gebunden: Alles, was die API zurückgibt, gehört zu ihm. Über die API erstellte Ressourcen sind im Dashboard sichtbar, Dashboard-Projekte werden jedoch nicht über die API bereitgestellt.

Jeder Authentifizierungsfehler — fehlender Header, unbekannter Schlüssel, widerrufener Schlüssel — liefert dieselbe 401-Antwort.

curl https://api.inwista.ai/v1/transcriptions \
  -H "Authorization: Bearer inw_live_4f6a…"

Fehler

Jeder Fehler verwendet dasselbe Envelope: ein Objekt mit einem maschinenlesbaren Code und einer menschenlesbaren Meldung. Codes werden innerhalb von v1 nur ergänzt, nie entfernt — baue auf dem Code auf, nicht auf der Meldung.

{ "error": { "code": "insufficient_credits", "message": "…" } }
StatusCodeWann
401invalid_api_keyAuthentifizierung fehlgeschlagen (beliebiger Grund)
400invalid_source_urlNicht https, Zugangsdaten in der URL, privater Host oder fehlerhaft formatiert
400conflicting_sourcesource_url und upload_id wurden beide gesendet; genau eines angeben
400invalid_upload_idupload_id ist keine gültige Upload-ID
400upload_not_foundKein Upload mit dieser id in diesem Workspace
400upload_incompleteNoch keine Datei per PUT in den Upload übertragen
409upload_already_usedDer Upload wurde bereits zu einer Transkription
409upload_in_progressEine andere Anfrage übermittelt diesen Upload gerade
400invalid_filenameFehlt oder hat keine Mediendateiendung
400invalid_content_typeKein MIME-Typ wie audio/mp4
400invalid_sizesize_bytes ist keine positive Ganzzahl
413file_too_largeAngegebene Größe liegt über 4 GB
429too_many_pending_uploadsDer Workspace hat bereits 25 Uploads, die auf die Übermittlung warten
400invalid_languageFehlt oder ist kein ISO 639-1-Code
400invalid_temperatureKeine Zahl zwischen 0 und 1
400invalid_num_speakersKeine Ganzzahl zwischen 1 und 32
400invalid_store_mediaKein Boolescher Wert
400invalid_retentionNicht "standard" oder "none", oder mit store_media true kombiniert
400invalid_metadataKein Objekt oder größer als 1 KB
400invalid_settingsKein Objekt oder größer als 2 KB
400unreadable_sourceMediendauer konnte nicht ermittelt werden
400invalid_cursorstarting_after ist keine bekannte id
400invalid_formatUntertitelformat wird nicht unterstützt
402insufficient_creditsDas Guthaben deckt die Kosten nicht
404not_foundUnbekannte Ressource
404translation_not_foundFür die ausgelieferte Version existiert keine abgeschlossene Übersetzung in der angeforderten Sprache
409not_readyErfordert eine abgeschlossene Transkription
409operation_in_progressEine Verbesserung wird noch verarbeitet
409translation_existsSprache existiert für diese Version bereits
400invalid_idempotency_keyIdempotency-Key-Header leer oder über 255 Zeichen
400idempotency_key_reusedIdempotency-Key wurde bereits für eine andere Anfrage verwendet
409idempotency_conflictEine Anfrage mit diesem Schlüssel wird noch verarbeitet
429rate_limitedZu viele Anfragen — nach dem Retry-After-Header erneut versuchen

Idempotenz

Jeder POST-Aufruf belastet Credits bei Annahme — eine Anfrage, die in einen Timeout läuft und blind wiederholt wird, würde also einen zweiten Auftrag und eine zweite Belastung erzeugen. Senden Sie einen Idempotency-Key-Header (eine beliebige eindeutige Zeichenkette, bis zu 255 Zeichen), und Wiederholungen werden sicher: Die Antwort der ersten Anfrage wird 24 Stunden gespeichert und für jede Wiederholung mit demselben Schlüssel unverändert zurückgegeben.

Die Wiederverwendung eines Schlüssels mit einer anderen Anfrage liefert 400 idempotency_key_reused; eine Wiederholung, während das Original noch läuft, liefert 409 idempotency_conflict. Fehlerantworten werden nie gespeichert — eine fehlgeschlagene Anfrage behält nie ihre Belastung, der Schlüssel wird also für einen sauberen neuen Versuch freigegeben.

curl -X POST https://api.inwista.ai/v1/transcriptions \
  -H "Authorization: Bearer inw_live_…" \
  -H "Idempotency-Key: order-42-transcribe" \
  -H "Content-Type: application/json" \
  -d '{ "source_url": "…", "language": "en" }'

Ratenbegrenzung

Jeder API-Schlüssel darf 300 Leseanfragen (GET) und 60 Schreibanfragen (POST) pro Minute stellen. Das Kontingent füllt sich kontinuierlich auf und kann auf einmal genutzt werden. Darüber antwortet die API mit 429 rate_limited und einem Retry-After-Header in Sekunden.

Betrachten Sie die Zahlen als Richtwerte: Drosseln Sie bei jedem 429, und bevorzugen Sie Webhooks gegenüber engem Polling.

HTTP/1.1 429 Too Many Requests
Retry-After: 12

{ "error": { "code": "rate_limited", "message": "…" } }

Credits

Vorgänge werden in Credits aus dem Prepaid-Guthaben deines Workspace abgerechnet (aufladen unter Dashboard → Billing). Transkription kostet 4 Credits pro angefangener Medienminute. Verbesserung und Übersetzung werden nach Inhaltsgröße berechnet — zu denselben Sätzen wie im Studio. Abrufen, Auflisten und Polling sind kostenlos.

Abgebucht wird, sobald eine Anfrage akzeptiert wird. Schlägt ein Vorgang fehl, wird der Betrag automatisch erstattet und die Ressource meldet credits_charged: 0. Bei einer 402-Ablehnung wird nie etwas abgebucht.

Paginierung

List-Endpunkte akzeptieren limit (Standard 25, max. 100) und starting_after — die letzte id der vorherigen Seite. Antworten verpacken die Ergebnisse in einem list-Objekt mit has_more.

{ "object": "list", "data": [ … ], "has_more": true }

Transkriptionen

Übermittle Medien per URL oder aus einem Upload, polle bis completed (oder nutze Webhooks) und rufe dann die Untertitel ab. Die Status sind processing, completed und failed. Zeitstempel sind unix-Sekunden.

Polling und Ergebnis teilen sich einen Endpunkt: GET /v1/transcriptions/{id} ist der Ort, an dem du den Status beobachtest UND an dem die fertige Ressource liegt — einen separaten Ergebnis-Endpunkt gibt es nicht. Die einzige Ausnahme sind die Untertiteldateien selbst: Sie kommen immer vom Captions-Endpunkt, weil es rohe Dateiinhalte sind, kein JSON.

Transkription erstellen

POST/v1/transcriptions

Verwandle jede gehostete Mediendatei in präzise, zeitgestempelte Untertitel, ohne dass jemand das Studio öffnet — und speise dein CMS, dein Archiv oder deine Publishing-Pipeline direkt.

Genau eines von source_url und upload_id ist erforderlich.

Die Mediendauer wird vor Annahme der Anfrage ermittelt; Guthaben-Abbuchung und Einreihen des Jobs erfolgen in einem atomaren Schritt. Gibt 201 mit der Transkriptions-Ressource zurück.

Credits: 4 Credits pro angefangener Medienminute, abgebucht bei Annahme des Jobs. Fehlgeschlagene Jobs werden automatisch erstattet.

Body

FeldTypBeschreibung
source_urlstringÖffentliche https-URL der Mediendatei oder eine YouTube-/TikTok-/Vimeo-URL. Gib dies oder upload_id an
upload_idstringDie id eines Uploads, dessen Datei per PUT hochgeladen wurde (siehe Uploads). Gib dies oder source_url an
languageerforderlichstringISO 639-1-Code, z. B. "en" oder "nb-NO"
diarizationbooleanSprecherlabels (Standard false)
num_speakersinteger1–32, Hinweis für diarization
temperaturenumberWhisper-Sampling-Temperatur zwischen 0 und 1; Standard 0 (deterministisch)
store_mediabooleanfalse = nur Transkription: keine Wiedergabedateien werden vorbereitet (Standard true)
retentionstring"none" löscht das Quellmedium nach der Transkription — das Transkript bleibt erhalten (Standard "standard")
metadataobjectDeine eigenen Tags, bis zu 1 KB, werden unverändert zurückgegeben
curl -X POST https://api.inwista.ai/v1/transcriptions \
  -H "Authorization: Bearer inw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "source_url": "https://cdn.example.com/interview.mp4",
    "language": "en",
    "diarization": true
  }'

Response

{
  "id": "aB3dE9f2…",
  "object": "transcription",
  "status": "processing",
  "progress": 50,
  "language": "en",
  "duration_seconds": 1834,
  "diarization": true,
  "store_media": true,
  "retention": "standard",
  "source_url": "https://cdn.example.com/interview.mp4",
  "upload_id": null,
  "temperature": 0,
  "metadata": { "internal_ref": "case-42" },
  "credits_charged": 124,
  "error": null,
  "created": 1754558000
}

Transkriptionen auflisten

GET/v1/transcriptions

Gleiche deinen Katalog mit den verarbeiteten Jobs ab oder baue ein Dashboard über alles, was du transkribiert hast.

Über die API erstellte Transkriptionen, neueste zuerst. Standard-Paginierung.

Credits: Kostenlos.

Query-Parameter

FeldTypBeschreibung
limitintegerSeitengröße, Standard 25, max. 100
starting_afterstringCursor: letzte id der vorherigen Seite
curl "https://api.inwista.ai/v1/transcriptions?limit=10" \
  -H "Authorization: Bearer inw_live_…"

Transkription abrufen

GET/v1/transcriptions/{id}

Fortschritts-Poll und Endergebnis in einem: Beobachte, wie der Status auf completed wechselt, und lies dann Dauer, Sprache und Abbuchung aus derselben Antwort.

Polle, bis der Status completed ist (oder registriere einen Webhook, siehe unten). Das Lesen einer fehlgeschlagenen Transkription löst zugleich ihre automatische Erstattung aus.

Credits: Kostenlos.

curl https://api.inwista.ai/v1/transcriptions/aB3dE9f2… \
  -H "Authorization: Bearer inw_live_…"

Response

{
  "id": "aB3dE9f2…",
  "object": "transcription",
  "status": "processing",
  "progress": 50,
  "language": "en",
  "duration_seconds": 1834,
  "diarization": true,
  "store_media": true,
  "retention": "standard",
  "source_url": "https://cdn.example.com/interview.mp4",
  "upload_id": null,
  "temperature": 0,
  "metadata": { "internal_ref": "case-42" },
  "credits_charged": 124,
  "error": null,
  "created": 1754558000
}

Eine Transkription löschen

DELETE/v1/transcriptions/{id}

Löschen Sie einen Auftrag auf Abruf — volle Datenkontrolle mit einem einzigen Aufruf.

Löscht die Transkription dauerhaft mit allem, was dafür gespeichert wurde: Mediendateien, Transkriptionsinhalt, Revisionen, Übersetzungen und Kommentare. Aggregierte Abrechnungszähler bleiben erhalten — sie enthalten keine Inhalte.

Nur abgeschlossene oder fehlgeschlagene Aufträge können gelöscht werden; ein noch laufender Auftrag liefert 409, ebenso einer mit laufender Verbesserung oder Übersetzung. Die Löschung ist sofort und unwiderruflich.

Credits: endpoints.delete-transcription.pricing

curl -X DELETE https://api.inwista.ai/v1/transcriptions/aB3dE9f2… \
  -H "Authorization: Bearer inw_live_…"

Response

{
  "id": "aB3dE9f2…",
  "object": "transcription",
  "deleted": true
}

Untertitel abrufen

GET/v1/transcriptions/{id}/captions

Hole sendefertige Untertiteldateien direkt in deinen Player, dein MAM oder deine Delivery-Pipeline — keine manuellen Exporte, keine Formatkonvertierung auf deiner Seite. In einer Videoproduktions-Pipeline landet fertiges SRT oder VTT direkt in deinem NLE, Review-Tool oder Packaging-Schritt, sobald ein Schnitt transkribiert ist.

Gibt den rohen Inhalt der Untertiteldatei mit passendem Content-Type zurück — nicht in JSON verpackt. Antwortet mit 409 not_ready, bis die Transkription abgeschlossen ist.

Credits: Kostenlos.

Query-Parameter

FeldTypBeschreibung
formaterforderlichstringsrt, vtt, json oder txt
diarization"true"Sprecherlabels voranstellen
languagestringLiefert eine abgeschlossene Übersetzung statt des Originals; 404 translation_not_found, wenn sie für die ausgelieferte Version fehlt
revisionstringRuft eine bestimmte Version ab (eine Verbesserungs-ID ist ihre Revisions-ID); ohne Angabe = die neueste
curl "https://api.inwista.ai/v1/transcriptions/aB3dE9f2…/captions?format=srt" \
  -H "Authorization: Bearer inw_live_…" -o interview.srt

Das json-Format ist ein versionierter Vertrag: { version: 1, language, segments: [{ index, start, end, text, speaker? }] } — Sekunden mit Millisekunden-Präzision, Felder werden innerhalb einer Version nur ergänzt, nie entfernt.

Übersetzungen gehören zu der Version, für die sie erstellt wurden — die revision_id der Übersetzungsressource benennt sie, und die Version einer späteren Verbesserung erbt sie nicht. Übergeben Sie diese revision_id als revision, um die übersetzte Version abzurufen.

Uploads

Direkter Datei-Upload für Medien ohne öffentliche URL: lokale Dateien, Desktop-Plugins, Dateien hinter einem Login. Ein Upload ist ein einmaliger Slot. Erstelle ihn, lade die Datei per PUT auf die zurückgegebene signierte URL hoch und übergib dann seine id als upload_id, wenn du die Transkription erstellst.

Die Bytes gehen direkt in den Speicher und laufen nie über den API-Host, deshalb funktionieren Dateien bis 4 GB, und abgebucht wird erst, wenn die Transkription übermittelt wird. Nie übermittelte Uploads werden nach einem Tag entfernt.

Upload erstellen

POST/v1/uploads

Transkribiere eine Datei direkt aus einer Desktop-App, einem Schnitt-Plugin oder von einem privaten Server, ohne sie vorher irgendwo öffentlich zu hosten.

Gibt 201 mit der Upload-Ressource zurück. Die signierte URL ist vier Stunden gültig und an die Header in upload_headers gebunden: Sende sie beim PUT genau so, wie sie zurückgegeben wurden, sonst weist der Speicher die Anfrage ab. Der length-range-Header setzt die Grenze von 4 GB durch.

Idempotency-Key wird berücksichtigt, ein wiederholtes Erstellen liefert also denselben Upload erneut, statt einen zweiten Slot anzulegen.

Credits: Kostenlos. Credits werden zum Transkriptionssatz abgebucht, sobald der Upload als Transkription übermittelt wird.

Body

FeldTypBeschreibung
filenameerforderlichstringMuss eine Mediendateiendung wie .m4a, .wav oder .mp4 tragen; bis zu 180 Zeichen
content_typestringMIME-Typ der Datei, Standard application/octet-stream. Der PUT muss genau diesen Content-Type senden
size_bytesintegerAngegebene Größe in Bytes; über 4 GB antwortet die API mit 413 file_too_large
# 1. Create the upload
curl -X POST https://api.inwista.ai/v1/uploads \
  -H "Authorization: Bearer inw_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "interview.m4a", "content_type": "audio/mp4" }'

# 2. PUT the file to upload_url with upload_headers
curl -X PUT "<upload_url>" \
  -H "Content-Type: audio/mp4" \
  -H "x-goog-content-length-range: 0,4294967296" \
  -H "x-goog-if-generation-match: 0" \
  --data-binary @interview.m4a

# 3. Submit it
curl -X POST https://api.inwista.ai/v1/transcriptions \
  -H "Authorization: Bearer inw_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "upload_id": "7hKq2mZp…", "language": "en" }'

Response

{
  "id": "7hKq2mZp…",
  "object": "upload",
  "status": "pending",
  "filename": "interview.m4a",
  "content_type": "audio/mp4",
  "size_bytes": 84213770,
  "transcription_id": null,
  "created": 1754558000,
  "upload_url": "https://storage.googleapis.com/…",
  "upload_method": "PUT",
  "upload_headers": {
    "Content-Type": "audio/mp4",
    "x-goog-content-length-range": "0,4294967296",
    "x-goog-if-generation-match": "0"
  },
  "upload_url_expires_at": 1754572400
}

Audio reicht für die Transkription: Exportierst du nur die Tonspur, bleibt ein abendfüllender Spielfilm unter 100 MB.

Ein Upload wird zu genau einer Transkription. Erneutes Übermitteln antwortet mit 409 upload_already_used.

Upload abrufen

GET/v1/uploads/{id}

Prüfe, ob eine Datei schon übermittelt wurde und zu welcher Transkription sie geworden ist.

Die Upload-Ressource ohne die einmaligen Upload-Felder. status steht auf consumed und transcription_id ist gesetzt, sobald der Upload übermittelt wurde.

Credits: Kostenlos.

curl https://api.inwista.ai/v1/uploads/7hKq2mZp… \
  -H "Authorization: Bearer inw_live_…"

Response

{
  "id": "7hKq2mZp…",
  "object": "upload",
  "status": "consumed",
  "filename": "interview.m4a",
  "content_type": "audio/mp4",
  "size_bytes": 84213770,
  "transcription_id": "aB3dE9f2…",
  "created": 1754558000
}

Verbesserungen

KI-Untertitelverbesserung — Zeilenlänge, Ausbalancierung und Timing-Regeln — erzeugt eine neue Version der Untertitel. Erfordert eine abgeschlossene Transkription. Sobald eine Verbesserung abgeschlossen ist, liefert der Untertitel-Abruf automatisch die verbesserte Version.

Verbesserung starten

POST/v1/transcriptions/{id}/enhance

Broadcast-taugliches Timing und Zeilenausgleich auf Autopilot — liefere Untertitel aus, die die QC bestehen, ohne dass jemand sie von Hand nachbearbeitet. Für Produktionshäuser automatisiert das den Untertitel-Conform-Schritt der Delivery-Pipeline: Jede Episode geht mit konsistenten, spezifikationskonformen Untertiteln raus.

Gibt 202 mit der Verbesserungs-Ressource zurück.

Credits: Berechnet aus der Größe des Untertitelinhalts zu denselben Sätzen wie im Studio, abgebucht bei Annahme des Jobs. Bei Fehlschlag automatische Erstattung.

Body

FeldTypBeschreibung
settingsobjectVerbesserungseinstellungen des Studios, bis zu 2 KB; weglassen für Standardwerte

Einstellungen für die Verbesserung

FeldTypBeschreibung
maxLinesPerBlock"1" | "2"Anzahl gleichzeitig angezeigter Zeilen; Broadcast-Standard und Voreinstellung ist 2
maxCharactersPerLine1–100Zeichen pro Zeile; Broadcast-Standard und Voreinstellung ist 42
blockLineBalancingbottom_heavy | top_heavy | equal | unconstrainedVisuelle Form zweizeiliger Blöcke; Voreinstellung ist unconstrained
textCondensationnone | smart | aggressiveErlaubt der KI, zu schnell lesbaren Dialog zu verdichten; Voreinstellung ist none (wortgetreu)
speakerDialogueFormatnone | hyphens | speaker_name | bracketsWie mehrere Sprecher in einem Block getrennt werden; Voreinstellung ist none
continuationMarkersnone | end | start | bothMarkerplatzierung, wenn ein Satz mehrere Blöcke umspannt; Voreinstellung ist none
continuationMarkerStyledash | ellipsisGedankenstrich oder Auslassungspunkte für geteilte Sätze; Voreinstellung ist dash
gapBetweenBlocksnone | broadcasting | streaming | sdhErzwungene Leerpause zwischen aufeinanderfolgenden Blöcken; Voreinstellung ist broadcasting (~99 ms)
minBlockDuration0.1–60 sKürzeste Anzeigedauer eines Blocks in Sekunden; Voreinstellung ist 1.0
maxBlockDuration> minLängste Anzeigedauer eines Blocks in Sekunden; Voreinstellung ist 7.0
curl -X POST https://api.inwista.ai/v1/transcriptions/aB3dE9f2…/enhance \
  -H "Authorization: Bearer inw_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "settings": { "maxCharactersPerLine": 37, "textCondensation": "smart" } }'

Response

{
  "id": "rev8Xk3…",
  "object": "enhancement",
  "transcription_id": "aB3dE9f2…",
  "status": "processing",
  "progress": 0,
  "credits_charged": 12,
  "error": null,
  "created": 1754559000
}

Verbesserung abrufen

GET/v1/transcriptions/{id}/enhancements/{enhancementId}

Poll und Ergebnis in einem: Sobald der Status completed meldet, liefert der Captions-Endpunkt bereits die verbesserte Version.

Polle bis completed. Das Lesen einer fehlgeschlagenen Verbesserung löst ihre automatische Erstattung aus.

Credits: Kostenlos.

curl https://api.inwista.ai/v1/transcriptions/aB3dE9f2…/enhancements/rev8Xk3… \
  -H "Authorization: Bearer inw_live_…"

Übersetzungen

Untertitelübersetzung mit vom Original übernommenem Timing — eine Übersetzung pro Sprache und Untertitelversion. Erfordert eine abgeschlossene Transkription.

Übersetzung starten

POST/v1/transcriptions/{id}/translate

Ein Aufruf pro Sprache macht aus einer fertigen Untertitelspur eine lokalisierte Version mit identischem Timing — vervielfache die Reichweite jedes Videos, das du bereits hast.

Gibt 202 zurück. Antwortet mit 409 translation_exists, wenn diese Sprache für die aktuelle Version bereits existiert.

Credits: Berechnet aus der Größe des Untertitelinhalts zu denselben Sätzen wie im Studio, pro Zielsprache, abgebucht bei Annahme. Bei Fehlschlag automatische Erstattung.

Body

FeldTypBeschreibung
target_languageerforderlichstringISO 639-1-Code, z. B. "es"
target_labelstringAnzeigename, bis zu 60 Zeichen
curl -X POST https://api.inwista.ai/v1/transcriptions/aB3dE9f2…/translate \
  -H "Authorization: Bearer inw_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "target_language": "es" }'

Response

{
  "id": "es",
  "object": "translation",
  "transcription_id": "aB3dE9f2…",
  "target_language": "es",
  "revision_id": "TRkf2nY7…",
  "status": "processing",
  "progress": 0,
  "credits_charged": 6,
  "error": null,
  "created": 1754559600
}

Übersetzung abrufen

GET/v1/transcriptions/{id}/translations/{language}

Poll und Ergebnis in einem: Sobald sie completed ist, holst du die übersetzte Untertiteldatei mit dem language-Parameter vom Captions-Endpunkt.

Polle bis completed und rufe die übersetzten Untertitel dann mit dem language-Query-Parameter vom Captions-Endpunkt ab.

Credits: Kostenlos.

# Poll the translation resource
curl https://api.inwista.ai/v1/transcriptions/aB3dE9f2…/translations/es \
  -H "Authorization: Bearer inw_live_…"

# Once completed, fetch the translated subtitle file
curl "https://api.inwista.ai/v1/transcriptions/aB3dE9f2…/captions?format=vtt&language=es" \
  -H "Authorization: Bearer inw_live_…" -o interview-es.vtt

Webhooks

Richte unter Dashboard → Integrations → Webhook einen Webhook ein, um transcript.completed-Events zu erhalten, statt zu pollen. Payloads sind HMAC-SHA256-signiert (X-Inwista-Signature: sha256=<hex> über den rohen Body) mit dem Signing-Secret, das bei der Einrichtung einmalig angezeigt wird. Datei-URLs sind vorsigniert und laufen nach 24 Stunden ab.

{
  "event": "transcript.completed",
  "id": "evt_9f2c…",
  "timestamp": 1754560000,
  "workspaceId": "ws_…",
  "project": {
    "id": "aB3dE9f2…",
    "name": "interview.mp4",
    "language": "en",
    "durationSeconds": 1834
  },
  "files": [
    {
      "format": "srt",
      "name": "interview.srt",
      "mimeType": "application/x-subrip; charset=utf-8",
      "url": "https://…",
      "expiresAt": 1754646400
    }
  ]
}