{"openapi":"3.1.0","info":{"title":"Transcriber","version":"1.0.0","description":"Transcribes audio."},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer"}},"schemas":{"Item":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["queued","processing","done","failed"]},"language":{"type":"string"},"createdAt":{"type":"string"},"completedAt":{"type":"string"},"expiresAt":{"type":"string","description":"When the item will be deleted"},"error":{"type":"string","description":"Why the transcription failed"},"transcript":{"type":"string","description":"One utterance per line, prefixed with its start time","example":"00:00:01 - Guten Tag, Beispiel GmbH, was kann ich für Sie tun?\n00:00:04 - Ich hätte eine Frage zu meiner Bestellung."}},"required":["id","status","language","createdAt"]},"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]}},"parameters":{}},"paths":{"/items":{"post":{"summary":"Upload audio for transcription","security":[{"apiKey":[]}],"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -i \"$TRANSCRIBER_URL/items\" \\\n  -H \"Authorization: Bearer $API_KEY\" \\\n  -F file=@call.mp3 \\\n  -F language=de \\\n  -F retention=7d"},{"lang":"JavaScript","label":"JavaScript","source":"import { openAsBlob } from 'node:fs';\n\nconst form = new FormData();\nform.set('file', await openAsBlob('call.mp3'), 'call.mp3');\nform.set('language', 'de');\nform.set('retention', '7d');\n\nconst res = await fetch(`${process.env.TRANSCRIBER_URL}/items`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.API_KEY}` },\n  body: form\n});\n// 202 Accepted with the item, otherwise an error\nconst body = await res.json();\nif (!res.ok) {\n  throw new Error(body.error);\n}\nconst { id } = body;"},{"lang":"TypeScript","label":"TypeScript","source":"import { openAsBlob } from 'node:fs';\n\ntype Item = {\n  id: string;\n  status: 'queued' | 'processing' | 'done' | 'failed';\n  // \"hh:mm:ss - utterance\" lines, once done\n  transcript?: string;\n  // Once failed\n  error?: string;\n};\n\nconst form = new FormData();\nform.set('file', await openAsBlob('call.mp3'), 'call.mp3');\nform.set('language', 'de');\nform.set('retention', '7d');\n\nconst res = await fetch(`${process.env.TRANSCRIBER_URL}/items`, {\n  method: 'POST',\n  headers: { Authorization: `Bearer ${process.env.API_KEY}` },\n  body: form\n});\n// 202 Accepted with the item, otherwise an error\nconst body = (await res.json()) as Item | { error: string };\nif (!('id' in body)) {\n  throw new Error(body.error);\n}\nconst { id } = body;"},{"lang":"Python","label":"Python","source":"import os\nimport requests\n\nwith open(\"call.mp3\", \"rb\") as f:\n    res = requests.post(\n        f\"{os.environ['TRANSCRIBER_URL']}/items\",\n        headers={\"Authorization\": f\"Bearer {os.environ['API_KEY']}\"},\n        files={\"file\": f},\n        data={\"language\": \"de\", \"retention\": \"7d\"},\n    )\nres.raise_for_status()\nitem_id = res.json()[\"id\"]  # 202 Accepted"},{"lang":"Java","label":"Java","source":"// JDK 25 compact source file (void main), which imports java.base automatically\nimport module java.net.http;\n\nvar boundary = UUID.randomUUID().toString();\nvar fields = \"\"\"\n        --%1$s\\r\n        Content-Disposition: form-data; name=\"language\"\\r\n        \\r\n        de\\r\n        --%1$s\\r\n        Content-Disposition: form-data; name=\"retention\"\\r\n        \\r\n        7d\\r\n        --%1$s\\r\n        Content-Disposition: form-data; name=\"file\"; filename=\"call.mp3\"\\r\n        \\r\n        \"\"\".formatted(boundary);\nvar body = HttpRequest.BodyPublishers.concat(\n        HttpRequest.BodyPublishers.ofString(fields),\n        HttpRequest.BodyPublishers.ofFile(Path.of(\"call.mp3\")),\n        HttpRequest.BodyPublishers.ofString(\"\\r\\n--\" + boundary + \"--\\r\\n\"));\n\nvar url = URI.create(System.getenv(\"TRANSCRIBER_URL\") + \"/items\");\nvar request = HttpRequest.newBuilder(url)\n        .header(\"Authorization\", \"Bearer \" + System.getenv(\"API_KEY\"))\n        .header(\"Content-Type\", \"multipart/form-data; boundary=\" + boundary)\n        .POST(body)\n        .build();\nvar client = HttpClient.newHttpClient();\nvar response = client.send(request, HttpResponse.BodyHandlers.ofString());\nif (response.statusCode() != 202) {\n    throw new IllegalStateException(response.body());\n}\n\nvar location = response.headers().firstValue(\"Location\").orElseThrow(); // /items/{id}\nvar id = location.substring(\"/items/\".length());"},{"lang":"Kotlin","label":"Kotlin","source":"// Ktor (ktor-client-cio, ktor-client-content-negotiation, ktor-serialization-kotlinx-json)\n// with the kotlinx.serialization plugin; runs in a coroutine. Imports left out, as in Ktor's docs.\n\n@Serializable\ndata class Item(\n    val id: String,\n    val status: String, // queued, processing, done or failed\n    val transcript: String? = null, // \"hh:mm:ss - utterance\" lines, once done\n    val error: String? = null, // once failed\n)\n\nval client = HttpClient(CIO) {\n    expectSuccess = true\n    install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }\n    defaultRequest {\n        url(\"${System.getenv(\"TRANSCRIBER_URL\")}/\")\n        bearerAuth(System.getenv(\"API_KEY\"))\n    }\n}\n\nval id = client.submitFormWithBinaryData(\n    \"items\",\n    formData {\n        append(\"file\", File(\"call.mp3\").readBytes(), Headers.build {\n            append(HttpHeaders.ContentDisposition, \"filename=\\\"call.mp3\\\"\")\n        })\n        append(\"language\", \"de\")\n        append(\"retention\", \"7d\")\n    },\n).body<Item>().id // 202 Accepted"}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary","description":"Mono audio, or stereo with audio on only one channel (mp3, wav, flac, ogg, m4a, …)"},"language":{"type":"string","pattern":"^[a-z]{2}$/u","default":"de","description":"ISO 639-1 code of the spoken language"},"prompt":{"type":"string","maxLength":2000,"description":"Hints for spelling and style, such as names and terms. Write it in the audio's language; only its last 224 tokens are used."},"retention":{"type":"string","pattern":"^(?<amount>\\d{1,4})(?<unit>[mhd])$/u","description":"How long to keep the item after it is done or failed, such as \"7d\", \"12h\" or \"30m\". Default: 7d, at most 30d"},"callback":{"type":"string","format":"uri","description":"HTTPS URL that receives a POST with `{ id, status }` when the item is done or failed"}},"required":["file"]}}}},"responses":{"202":{"description":"Queued; poll the URL in `Location`","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds until polling again makes sense, while pending"},"required":false,"description":"Seconds until polling again makes sense, while pending"},"Location":{"schema":{"type":"string"},"required":true}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Item"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"File too large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"415":{"description":"Not an audio file","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Invalid parameter, or audio on more than one channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/items/{id}":{"get":{"summary":"Get status and, once done, the transcript","description":"Always 200 for an existing item; `status` tells whether it is finished.","security":[{"apiKey":[]}],"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl \"$TRANSCRIBER_URL/items/$ID\" -H \"Authorization: Bearer $API_KEY\"\n\n# Wait for the transcript\nuntil curl -s \"$TRANSCRIBER_URL/items/$ID\" -H \"Authorization: Bearer $API_KEY\" \\\n  | jq -e '.status | IN(\"done\", \"failed\")' >/dev/null; do sleep 5; done\ncurl -s \"$TRANSCRIBER_URL/items/$ID\" -H \"Authorization: Bearer $API_KEY\" | jq -r .transcript"},{"lang":"JavaScript","label":"JavaScript","source":"import { setTimeout as sleep } from 'node:timers/promises';\n\nconst MILLISECONDS_PER_SECOND = 1000;\n\n// Retry-After is only sent while the item is queued or processing\nasync function awaitItem() {\n  const response = await fetch(`${process.env.TRANSCRIBER_URL}/items/${id}`, {\n    headers: { Authorization: `Bearer ${process.env.API_KEY}` }\n  });\n  const retryAfter = response.headers.get('retry-after');\n  if (retryAfter === null) {\n    return response.json();\n  }\n  await sleep(Number(retryAfter) * MILLISECONDS_PER_SECOND);\n  return awaitItem();\n}\n\nconst item = await awaitItem();\nconsole.log(item.transcript ?? item.error);"},{"lang":"TypeScript","label":"TypeScript","source":"import { setTimeout as sleep } from 'node:timers/promises';\n\nconst MILLISECONDS_PER_SECOND = 1000;\n\n// Retry-After is only sent while the item is queued or processing\nasync function awaitItem(): Promise<Item> {\n  const response = await fetch(`${process.env.TRANSCRIBER_URL}/items/${id}`, {\n    headers: { Authorization: `Bearer ${process.env.API_KEY}` }\n  });\n  const retryAfter = response.headers.get('retry-after');\n  if (retryAfter === null) {\n    return (await response.json()) as Item;\n  }\n  await sleep(Number(retryAfter) * MILLISECONDS_PER_SECOND);\n  return awaitItem();\n}\n\nconst item = await awaitItem();\nconsole.log(item.transcript ?? item.error);"},{"lang":"Python","label":"Python","source":"import os\nimport time\nimport requests\n\nurl = f\"{os.environ['TRANSCRIBER_URL']}/items/{item_id}\"\nheaders = {\"Authorization\": f\"Bearer {os.environ['API_KEY']}\"}\n\n# Retry-After is only sent while the item is queued or processing\nres = requests.get(url, headers=headers)\nwhile \"Retry-After\" in res.headers:\n    time.sleep(int(res.headers[\"Retry-After\"]))\n    res = requests.get(url, headers=headers)\n\nitem = res.json()\nprint(item.get(\"transcript\", item.get(\"error\")))"},{"lang":"Java","label":"Java","source":"import module java.net.http;\n\nvar url = URI.create(System.getenv(\"TRANSCRIBER_URL\") + \"/items/\" + id);\nvar request = HttpRequest.newBuilder(url)\n        .header(\"Authorization\", \"Bearer \" + System.getenv(\"API_KEY\"))\n        .build();\nvar client = HttpClient.newHttpClient();\n\n// Retry-After is only sent while the item is queued or processing\nvar response = client.send(request, HttpResponse.BodyHandlers.ofString());\nwhile (response.headers().firstValue(\"Retry-After\").isPresent()) {\n    var seconds = Long.parseLong(response.headers().firstValue(\"Retry-After\").get());\n    Thread.sleep(Duration.ofSeconds(seconds));\n    response = client.send(request, HttpResponse.BodyHandlers.ofString());\n}\n\nIO.println(response.body()); // JSON: \"transcript\" once done, \"error\" if failed"},{"lang":"Kotlin","label":"Kotlin","source":"// Retry-After is only sent while the item is queued or processing\ntailrec suspend fun awaitItem(id: String): Item {\n    val response = client.get(\"items/$id\")\n    val retryAfter = response.headers[HttpHeaders.RetryAfter] ?: return response.body()\n    delay(retryAfter.toLong().seconds)\n    return awaitItem(id)\n}\n\nval item = awaitItem(id)\nprintln(item.transcript ?: item.error)"}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The item","headers":{"Retry-After":{"schema":{"type":"string","description":"Seconds until polling again makes sense, while pending"},"required":false,"description":"Seconds until polling again makes sense, while pending"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Item"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete an item, its audio and transcript","security":[{"apiKey":[]}],"x-codeSamples":[{"lang":"Shell","label":"curl","source":"curl -X DELETE \"$TRANSCRIBER_URL/items/$ID\" -H \"Authorization: Bearer $API_KEY\""},{"lang":"JavaScript","label":"JavaScript","source":"// 204 No Content\nawait fetch(`${process.env.TRANSCRIBER_URL}/items/${id}`, {\n  method: 'DELETE',\n  headers: { Authorization: `Bearer ${process.env.API_KEY}` }\n});"},{"lang":"TypeScript","label":"TypeScript","source":"// 204 No Content\nawait fetch(`${process.env.TRANSCRIBER_URL}/items/${id}`, {\n  method: 'DELETE',\n  headers: { Authorization: `Bearer ${process.env.API_KEY}` }\n});"},{"lang":"Python","label":"Python","source":"import os\nimport requests\n\nrequests.delete(\n    f\"{os.environ['TRANSCRIBER_URL']}/items/{item_id}\",\n    headers={\"Authorization\": f\"Bearer {os.environ['API_KEY']}\"},\n).raise_for_status()  # 204 No Content"},{"lang":"Java","label":"Java","source":"import module java.net.http;\n\nvar url = URI.create(System.getenv(\"TRANSCRIBER_URL\") + \"/items/\" + id);\nvar request = HttpRequest.newBuilder(url)\n        .header(\"Authorization\", \"Bearer \" + System.getenv(\"API_KEY\"))\n        .DELETE()\n        .build();\nHttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.discarding()); // 204 No Content"},{"lang":"Kotlin","label":"Kotlin","source":"client.delete(\"items/$id\") // 204 No Content"}],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such item","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}