
Base URL: https://api.inwista.ai/v1Erstelle 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…"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": "…" } }| Status | Code | Wann |
|---|---|---|
| 401 | invalid_api_key | Authentifizierung fehlgeschlagen (beliebiger Grund) |
| 400 | invalid_source_url | Nicht https, Zugangsdaten in der URL, privater Host oder fehlerhaft formatiert |
| 400 | conflicting_source | source_url und upload_id wurden beide gesendet; genau eines angeben |
| 400 | invalid_upload_id | upload_id ist keine gültige Upload-ID |
| 400 | upload_not_found | Kein Upload mit dieser id in diesem Workspace |
| 400 | upload_incomplete | Noch keine Datei per PUT in den Upload übertragen |
| 409 | upload_already_used | Der Upload wurde bereits zu einer Transkription |
| 409 | upload_in_progress | Eine andere Anfrage übermittelt diesen Upload gerade |
| 400 | invalid_filename | Fehlt oder hat keine Mediendateiendung |
| 400 | invalid_content_type | Kein MIME-Typ wie audio/mp4 |
| 400 | invalid_size | size_bytes ist keine positive Ganzzahl |
| 413 | file_too_large | Angegebene Größe liegt über 4 GB |
| 429 | too_many_pending_uploads | Der Workspace hat bereits 25 Uploads, die auf die Übermittlung warten |
| 400 | invalid_language | Fehlt oder ist kein ISO 639-1-Code |
| 400 | invalid_temperature | Keine Zahl zwischen 0 und 1 |
| 400 | invalid_num_speakers | Keine Ganzzahl zwischen 1 und 32 |
| 400 | invalid_store_media | Kein Boolescher Wert |
| 400 | invalid_retention | Nicht "standard" oder "none", oder mit store_media true kombiniert |
| 400 | invalid_metadata | Kein Objekt oder größer als 1 KB |
| 400 | invalid_settings | Kein Objekt oder größer als 2 KB |
| 400 | unreadable_source | Mediendauer konnte nicht ermittelt werden |
| 400 | invalid_cursor | starting_after ist keine bekannte id |
| 400 | invalid_format | Untertitelformat wird nicht unterstützt |
| 402 | insufficient_credits | Das Guthaben deckt die Kosten nicht |
| 404 | not_found | Unbekannte Ressource |
| 404 | translation_not_found | Für die ausgelieferte Version existiert keine abgeschlossene Übersetzung in der angeforderten Sprache |
| 409 | not_ready | Erfordert eine abgeschlossene Transkription |
| 409 | operation_in_progress | Eine Verbesserung wird noch verarbeitet |
| 409 | translation_exists | Sprache existiert für diese Version bereits |
| 400 | invalid_idempotency_key | Idempotency-Key-Header leer oder über 255 Zeichen |
| 400 | idempotency_key_reused | Idempotency-Key wurde bereits für eine andere Anfrage verwendet |
| 409 | idempotency_conflict | Eine Anfrage mit diesem Schlüssel wird noch verarbeitet |
| 429 | rate_limited | Zu viele Anfragen — nach dem Retry-After-Header erneut versuchen |
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" }'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": "…" } }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.
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 }Ü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.
/v1/transcriptionsVerwandle 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
| Feld | Typ | Beschreibung |
|---|---|---|
source_url | string | Öffentliche https-URL der Mediendatei oder eine YouTube-/TikTok-/Vimeo-URL. Gib dies oder upload_id an |
upload_id | string | Die id eines Uploads, dessen Datei per PUT hochgeladen wurde (siehe Uploads). Gib dies oder source_url an |
languageerforderlich | string | ISO 639-1-Code, z. B. "en" oder "nb-NO" |
diarization | boolean | Sprecherlabels (Standard false) |
num_speakers | integer | 1–32, Hinweis für diarization |
temperature | number | Whisper-Sampling-Temperatur zwischen 0 und 1; Standard 0 (deterministisch) |
store_media | boolean | false = nur Transkription: keine Wiedergabedateien werden vorbereitet (Standard true) |
retention | string | "none" löscht das Quellmedium nach der Transkription — das Transkript bleibt erhalten (Standard "standard") |
metadata | object | Deine 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
}/v1/transcriptionsGleiche 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
| Feld | Typ | Beschreibung |
|---|---|---|
limit | integer | Seitengröße, Standard 25, max. 100 |
starting_after | string | Cursor: letzte id der vorherigen Seite |
curl "https://api.inwista.ai/v1/transcriptions?limit=10" \
-H "Authorization: Bearer inw_live_…"/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
}/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
}/v1/transcriptions/{id}/captionsHole 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
| Feld | Typ | Beschreibung |
|---|---|---|
formaterforderlich | string | srt, vtt, json oder txt |
diarization | "true" | Sprecherlabels voranstellen |
language | string | Liefert eine abgeschlossene Übersetzung statt des Originals; 404 translation_not_found, wenn sie für die ausgelieferte Version fehlt |
revision | string | Ruft 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.srtDas 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.
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.
/v1/uploadsTranskribiere 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
| Feld | Typ | Beschreibung |
|---|---|---|
filenameerforderlich | string | Muss eine Mediendateiendung wie .m4a, .wav oder .mp4 tragen; bis zu 180 Zeichen |
content_type | string | MIME-Typ der Datei, Standard application/octet-stream. Der PUT muss genau diesen Content-Type senden |
size_bytes | integer | Angegebene 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.
/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
}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.
/v1/transcriptions/{id}/enhanceBroadcast-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
| Feld | Typ | Beschreibung |
|---|---|---|
settings | object | Verbesserungseinstellungen des Studios, bis zu 2 KB; weglassen für Standardwerte |
Einstellungen für die Verbesserung
| Feld | Typ | Beschreibung |
|---|---|---|
maxLinesPerBlock | "1" | "2" | Anzahl gleichzeitig angezeigter Zeilen; Broadcast-Standard und Voreinstellung ist 2 |
maxCharactersPerLine | 1–100 | Zeichen pro Zeile; Broadcast-Standard und Voreinstellung ist 42 |
blockLineBalancing | bottom_heavy | top_heavy | equal | unconstrained | Visuelle Form zweizeiliger Blöcke; Voreinstellung ist unconstrained |
textCondensation | none | smart | aggressive | Erlaubt der KI, zu schnell lesbaren Dialog zu verdichten; Voreinstellung ist none (wortgetreu) |
speakerDialogueFormat | none | hyphens | speaker_name | brackets | Wie mehrere Sprecher in einem Block getrennt werden; Voreinstellung ist none |
continuationMarkers | none | end | start | both | Markerplatzierung, wenn ein Satz mehrere Blöcke umspannt; Voreinstellung ist none |
continuationMarkerStyle | dash | ellipsis | Gedankenstrich oder Auslassungspunkte für geteilte Sätze; Voreinstellung ist dash |
gapBetweenBlocks | none | broadcasting | streaming | sdh | Erzwungene Leerpause zwischen aufeinanderfolgenden Blöcken; Voreinstellung ist broadcasting (~99 ms) |
minBlockDuration | 0.1–60 s | Kürzeste Anzeigedauer eines Blocks in Sekunden; Voreinstellung ist 1.0 |
maxBlockDuration | > min | Lä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
}/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_…"Untertitelübersetzung mit vom Original übernommenem Timing — eine Übersetzung pro Sprache und Untertitelversion. Erfordert eine abgeschlossene Transkription.
/v1/transcriptions/{id}/translateEin 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
| Feld | Typ | Beschreibung |
|---|---|---|
target_languageerforderlich | string | ISO 639-1-Code, z. B. "es" |
target_label | string | Anzeigename, 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
}/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.vttRichte 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
}
]
}