{"openapi":"3.1.0","info":{"title":"DeepScript API","version":"1.0.0","description":"# DeepScript Transcription API\n\nSecure, privacy-focused speech-to-text API. All data processed on our own servers in Germany.\n\n## Authentication\n\nUse Bearer token authentication with either:\n- **API Key**: `Authorization: Bearer ds_live_xxx` (recommended for server-to-server)\n- **JWT Token**: From NextAuth session (for frontend use)\n\n## Quick Start\n\n```bash\n# 1. Upload a file for transcription\ncurl -X POST https://api.deepscript.com/v1/transcriptions \\\n  -H \"Authorization: Bearer ds_live_YOUR_KEY\" \\\n  -F \"file=@meeting.mp3\" \\\n  -F \"model=standard\" \\\n  -F \"language=auto\"\n\n# 2. Check status (poll until completed)\ncurl https://api.deepscript.com/v1/transcriptions/{id}/status \\\n  -H \"Authorization: Bearer ds_live_YOUR_KEY\"\n\n# 3. Download result\ncurl https://api.deepscript.com/v1/transcriptions/{id}/export?format=srt \\\n  -H \"Authorization: Bearer ds_live_YOUR_KEY\" -o result.srt\n```\n\n## Rate Limits\n\n| Tier | Limit |\n|------|-------|\n| Unauthenticated | 30 requests/minute |\n| Authenticated | 100 requests/minute |\n\nRate limit headers: `x-ratelimit-limit`, `x-ratelimit-remaining`, `x-ratelimit-reset`\n\n## Error Format (RFC 7807)\n\nAll errors follow the Problem Details for HTTP APIs format ([RFC 7807](https://www.rfc-editor.org/rfc/rfc7807)). Responses carry `Content-Type: application/problem+json` and the body has at minimum:\n\n```json\n{\n  \"type\": \"https://deepscript.com/errors/unauthorized\",\n  \"title\": \"Unauthorized\",\n  \"status\": 401,\n  \"detail\": \"Missing Authorization header.\"\n}\n```\n\n`type` is a stable slug-URL identifying the problem category — discriminate on the slug, not on `status` or `title`. The defined slugs are:\n\n| Slug | Status | When it fires |\n|---|---|---|\n| `unauthorized` | 401 | Missing / malformed `Authorization` header. |\n| `invalid-api-key` | 401 | API key not found, revoked, or expired. |\n| `invalid-token` | 401 | JWT signature invalid or token expired. |\n| `user-not-found` | 401 | Token refers to a user that no longer exists. |\n| `insufficient-balance` | 402 | Workspace balance is below the minimum required to start a transcription. Body carries `balance`, `freeRemaining`, `minimumBalance` extensions. |\n| `forbidden` | 403 | Authenticated, but the user lacks permission (e.g. admin-only endpoint). |\n| `not-found` | 404 | Endpoint or resource does not exist. |\n| `missing-file` | 400 | POST /v1/transcriptions called without a multipart `file` field. |\n| `not-ready` | 400 | Transcription is not yet `completed` (e.g. export attempted while still processing). |\n| `unsupported-format` | 422 | Upload MIME type is not one of the accepted audio/video types. |\n| `file-too-large` | 422 | Upload exceeds the 500 MB limit. |\n| `invalid-model` | 422 | `model` field is not `standard` or `premium`. |\n| `invalid-speaker-hint` | 422 | Speaker-count fields are inconsistent (e.g. both exact and range). |\n| `invalid-format` | 422 | Export `format` query is not one of txt/srt/vtt/json. |\n| `validation` | 422 | Request body fails JSON schema validation. Body carries an `errors` extension with field-level detail. |\n| `rate-limit` | 429 | Tier limit exceeded — see `Retry-After` header. |\n| `internal` | 500 | Unexpected server error. Always retryable; if persistent, contact support. |\n\nExtension members (RFC 7807 §3.2) carry context — `insufficient-balance` always includes `balance` and `minimumBalance`; `validation` always includes `errors`.\n\n## Webhooks\n\nRegister a webhook endpoint via `POST /v1/webhooks` to receive a real-time HTTP\ncallback whenever an event fires. The secret returned at creation time is used\nto sign every delivery.\n\n**Events**\n\n| Event | When it fires |\n|---|---|\n| `transcription.completed` | A transcription finishes successfully and `resultText` is available. |\n| `transcription.failed`    | A transcription enters the `failed` state. `errorMessage` carries the reason. |\n| `balance.low`             | Workspace balance drops below the configured low-balance threshold. |\n\n**What the call looks like**\n\nDeepScript sends an HTTPS `POST` to your registered URL with a JSON body and\ntwo signed headers:\n\n```http\nPOST /your/endpoint HTTP/1.1\nHost: hooks.yourapp.com\nContent-Type: application/json\nX-DeepScript-Timestamp: 1731340800\nX-DeepScript-Signature: 4f7c2d39e6a8b1c9d8e3f0a2b4c6d8e0f1a3b5c7d9e1f3a5b7c9d1e3f5a7b9c1\n\n{\n  \"event\": \"transcription.completed\",\n  \"timestamp\": \"1731340800\",\n  \"data\": {\n    \"id\": \"f3a1c5b2-9c0e-4a1d-9f4f-7e0e8e6e6c11\",\n    \"model\": \"standard\",\n    \"status\": \"completed\",\n    \"duration\": 312.4,\n    \"wordCount\": 1543,\n    \"speakerCount\": 3,\n    \"language\": \"de\",\n    \"cost\": 0.94,\n    \"completedAt\": \"2026-05-14T10:00:00.000Z\"\n  }\n}\n```\n\n**Verifying the signature** — concatenate `{timestamp}.{rawBody}`,\nHMAC-SHA256 with your webhook secret (hex output), compare against\n`X-DeepScript-Signature`. Reject the call if it doesn't match or if the\ntimestamp is older than 5 minutes (replay protection).\n\n```js\n// Node.js\nimport crypto from \"node:crypto\";\n\nfunction verify(req, rawBody, secret) {\n  const signature = req.headers[\"x-deepscript-signature\"];\n  const timestamp = req.headers[\"x-deepscript-timestamp\"];\n  if (!signature || !timestamp) return false;\n  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;\n  const expected = crypto\n    .createHmac(\"sha256\", secret)\n    .update(`${timestamp}.${rawBody}`)\n    .digest(\"hex\");\n  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));\n}\n```\n\n```python\n# Python\nimport hmac, hashlib, time\n\ndef verify(headers, raw_body: bytes, secret: str) -> bool:\n    sig = headers.get(\"x-deepscript-signature\") or \"\"\n    ts = headers.get(\"x-deepscript-timestamp\")\n    if not sig or not ts: return False\n    if abs(int(time.time()) - int(ts)) > 300: return False\n    expected = hmac.new(secret.encode(), f\"{ts}.\".encode() + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(sig, expected)\n```\n\n**Delivery & retries**\n\n- Your endpoint must respond with **2xx** within **10 seconds**.\n- Non-2xx or timeouts trigger up to **3 attempts** total with linear backoff\n  (5s, then 10s). After the last failure the delivery is dropped — no manual\n  retry available, configure logging on your side if needed.\n- Deliveries can arrive **out of order** under retry; use `data.completedAt`\n  / `data.failedAt` to reconstruct timeline.\n- The same event may be delivered **more than once**; key on `data.id`\n  (transcription id) if you need exactly-once semantics on your side.\n","contact":{"name":"DeepScript Support","url":"https://deepscript.com/support","email":"support@deepscript.com"},"license":{"name":"Proprietary"}},"servers":[{"url":"https://api.deepscript.com","description":"Production"},{"url":"http://localhost:4000","description":"Local Development"}],"security":[{"BearerAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"API Key (ds_live_xxx) or JWT token"}},"schemas":{"Transcription":{"type":"object","description":"Transcription summary as returned in list responses. The detail endpoint adds resultJson + mimeType — see TranscriptionDetail.","properties":{"id":{"type":"string","format":"uuid"},"model":{"type":"string","enum":["standard","premium"]},"status":{"type":"string","enum":["queued","processing","completed","failed"]},"progress":{"type":"integer","minimum":0,"maximum":100,"description":"0-100, only meaningful while status='processing'."},"originalFileName":{"type":"string"},"fileSize":{"type":"integer","description":"Upload size in bytes."},"duration":{"type":"number","nullable":true,"description":"Audio duration in seconds. Set once the engine has decoded the file."},"language":{"type":"string","nullable":true,"description":"ISO 639-1 code if the caller specified one; null when 'auto' was used."},"detectedLanguage":{"type":"string","nullable":true,"description":"ISO 639-1 code the engine actually detected. Falls back to `language` if auto-detect failed."},"resultText":{"type":"string","nullable":true,"description":"Full plain-text transcript. Null while queued/processing."},"wordCount":{"type":"integer","nullable":true},"speakerCount":{"type":"integer","nullable":true,"description":"Number of distinct speakers detected via diarization. 1 for solo recordings."},"cost":{"type":"number","nullable":true,"description":"Billed amount in the directory's currency. Set once completed and charged."},"errorMessage":{"type":"string","nullable":true,"description":"Set when status='failed'. May be prefixed by an engine error code (e.g. 'NO_SPOKEN_AUDIO: ...')."},"createdOn":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time","nullable":true}}},"TranscriptionWord":{"type":"object","description":"Single word with audio offsets. Speaker is null when diarization was unavailable.","required":["word","start","end"],"properties":{"word":{"type":"string","description":"The word as written, including trailing punctuation."},"start":{"type":"number","description":"Audio offset of the word's first sample, in seconds."},"end":{"type":"number","description":"Audio offset of the word's last sample, in seconds."},"speaker":{"type":"string","nullable":true,"description":"Raw speaker ID from the engine (e.g. '0', 'SPEAKER_00'). Stable per speaker within one transcription; normalize via the wordCount-aware utilities in the JS client if you need human labels."}}},"TranscriptionDetail":{"allOf":[{"$ref":"#/components/schemas/Transcription"},{"type":"object","description":"Detail-view extension — only returned by GET /v1/transcriptions/{id}, never by the list endpoint (payload would be too heavy).","properties":{"mimeType":{"type":"string","nullable":true,"description":"Detected MIME type of the upload (e.g. 'audio/mpeg', 'video/mp4')."},"userId":{"type":"string","format":"uuid","nullable":true,"description":"Owner of the transcription. Null for guest-tool uploads."},"directoryId":{"type":"string","format":"uuid","description":"The workspace this transcription belongs to. Use to scope authorization and billing."},"resultJson":{"type":"object","nullable":true,"description":"Structured result with word-level offsets. Null while queued/processing or if the engine returned no words.","properties":{"words":{"type":"array","items":{"$ref":"#/components/schemas/TranscriptionWord"}}}}}}]},"Vocabulary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"words":{"type":"array","items":{"type":"string"}},"createdOn":{"type":"string","format":"date-time"}}},"ProblemDetails":{"type":"object","description":"RFC 7807 Problem Details for HTTP APIs. `type` is a stable slug-URL — discriminate on it instead of status/title. Specific problem types add extension members (see InsufficientBalanceProblem, ValidationProblem).","required":["type","title","status","detail"],"properties":{"type":{"type":"string","format":"uri","description":"Stable slug-URL identifying the problem category. See the error table in the API description for the list of defined slugs.","example":"https://deepscript.com/errors/unauthorized"},"title":{"type":"string","description":"Short, human-readable summary of the problem type. Stable across occurrences.","example":"Unauthorized"},"status":{"type":"integer","description":"HTTP status code, repeated in the body for clients that buffer responses.","example":401},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence. Varies between calls.","example":"Missing Authorization header. Use 'Bearer <token>' or 'Bearer <api-key>'."}}},"InsufficientBalanceProblem":{"allOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"type":"object","description":"Extension members on the `insufficient-balance` problem.","properties":{"balance":{"type":"number","description":"Current workspace balance in EUR."},"freeRemaining":{"type":"integer","description":"Free-tier transcriptions still available."},"minimumBalance":{"type":"number","description":"Minimum balance required to start a transcription."}}}]},"ValidationProblem":{"allOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"type":"object","description":"Extension members on the `validation` problem.","properties":{"errors":{"type":"object","description":"Flattened Zod issue map: `fieldErrors` keyed by JSON-path → array of messages; `formErrors` for body-level issues.","properties":{"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},"formErrors":{"type":"array","items":{"type":"string"}}}}}}]},"WebhookEnvelope":{"type":"object","description":"Outer envelope of every webhook delivery — same shape across all events.","required":["event","timestamp","data"],"properties":{"event":{"type":"string","enum":["transcription.completed","transcription.failed","balance.low"],"description":"Event type. Same value as the body content of the X-DeepScript-Timestamp signature input."},"timestamp":{"type":"string","description":"Unix seconds at signing time. Repeated in the X-DeepScript-Timestamp header.","example":"1731340800"},"data":{"description":"Event-specific payload. See per-event schema in the webhooks section.","type":"object"}}},"TranscriptionCompletedPayload":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"type":"string","enum":["transcription.completed"]},"data":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string","format":"uuid"},"model":{"type":"string","enum":["standard","premium"]},"status":{"type":"string","enum":["completed"]},"duration":{"type":"number","description":"Audio duration in seconds."},"wordCount":{"type":"integer"},"speakerCount":{"type":"integer"},"language":{"type":"string"},"cost":{"type":"number","description":"Billed amount in workspace currency."},"completedAt":{"type":"string","format":"date-time"}}}}}]},"TranscriptionFailedPayload":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"type":"string","enum":["transcription.failed"]},"data":{"type":"object","required":["id","status","errorMessage"],"properties":{"id":{"type":"string","format":"uuid"},"model":{"type":"string","enum":["standard","premium"]},"status":{"type":"string","enum":["failed"]},"errorMessage":{"type":"string"},"failedAt":{"type":"string","format":"date-time"}}}}}]},"BalanceLowPayload":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"type":"string","enum":["balance.low"]},"data":{"type":"object","required":["balance","threshold","currency"],"properties":{"balance":{"type":"number","description":"Remaining workspace balance."},"threshold":{"type":"number","description":"Configured low-balance threshold."},"currency":{"type":"string","description":"ISO 4217 currency code, e.g. EUR."}}}}}]}},"responses":{"Unauthorized":{"description":"Missing or invalid credentials.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"Forbidden":{"description":"Authenticated but not permitted for this resource.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"NotFound":{"description":"Resource or endpoint does not exist.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"RateLimitExceeded":{"description":"Request exceeded the per-minute tier limit. Inspect `Retry-After` for backoff guidance.","headers":{"Retry-After":{"description":"Seconds the client should wait before retrying.","schema":{"type":"integer"}},"x-ratelimit-limit":{"description":"Maximum requests allowed in the current window.","schema":{"type":"integer"}},"x-ratelimit-remaining":{"description":"Remaining requests in the current window.","schema":{"type":"integer"}},"x-ratelimit-reset":{"description":"Seconds until the rate limit window resets.","schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"ValidationError":{"description":"Request failed validation. `errors` extension carries field-level detail.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ValidationProblem"}}}},"InsufficientBalance":{"description":"Workspace balance is below the minimum required to start work.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/InsufficientBalanceProblem"}}}},"InternalServerError":{"description":"Unexpected server error. Always retryable.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}},"parameters":{"WebhookTimestamp":{"name":"X-DeepScript-Timestamp","in":"header","required":true,"schema":{"type":"integer"},"description":"Unix seconds at the moment the request was signed. Reject if older than 5 minutes."},"WebhookSignature":{"name":"X-DeepScript-Signature","in":"header","required":true,"schema":{"type":"string","example":"4f7c2d39e6a8…b9c1"},"description":"Hex HMAC-SHA256 of `{timestamp}.{rawBody}` using your webhook secret."}}},"paths":{"/v1/health":{"get":{"tags":["System"],"summary":"Health check","description":"Liveness probe. Does not touch the database. Always 200 if the process is up.","security":[],"responses":{"200":{"description":"Service is healthy.","content":{"application/json":{"schema":{"type":"object","required":["status","timestamp","version"],"properties":{"status":{"type":"string","enum":["ok"]},"timestamp":{"type":"string","format":"date-time"},"version":{"type":"string"}}}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/languages":{"get":{"tags":["System"],"summary":"List supported languages","description":"Returns all 99 supported transcription languages (Whisper). Stable response — safe to cache for a long time.","security":[],"responses":{"200":{"description":"Language list.","content":{"application/json":{"schema":{"type":"object","required":["data","count"],"properties":{"data":{"type":"array","items":{"type":"object","required":["code","name"],"properties":{"code":{"type":"string","description":"ISO 639-1 language code."},"name":{"type":"string","description":"English language name."}}}},"count":{"type":"integer"}}}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/account/balance":{"get":{"tags":["Account"],"summary":"Get current balance","description":"Balance is workspace-scoped (one balance per directory the API key is bound to).","responses":{"200":{"description":"Balance info.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"balance":{"type":"number","description":"Remaining workspace balance in EUR."},"freeTranscriptionsRemaining":{"type":"integer"},"autoReloadEnabled":{"type":"boolean"},"autoReloadThreshold":{"type":"number","nullable":true},"autoReloadAmount":{"type":"number","nullable":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/account/usage":{"get":{"tags":["Account"],"summary":"Get usage analytics","description":"Monthly aggregates for completed transcriptions in the active workspace. Use for billing dashboards.","parameters":[{"name":"months","in":"query","schema":{"type":"integer","default":6,"minimum":1,"maximum":12},"description":"Number of months to look back. Hard-capped at 12."}],"responses":{"200":{"description":"Monthly usage breakdown plus a summary row.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"months":{"type":"array","items":{"type":"object","properties":{"month":{"type":"string","description":"YYYY-MM."},"standardMinutes":{"type":"number"},"premiumMinutes":{"type":"number"},"standardCost":{"type":"number"},"premiumCost":{"type":"number"},"count":{"type":"integer"}}}},"summary":{"type":"object","properties":{"totalTranscriptions":{"type":"integer"},"totalMinutes":{"type":"number"},"totalCost":{"type":"number"}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/account/profile":{"get":{"tags":["Account"],"summary":"Get account profile","description":"User identity + active-workspace snapshot. Useful as a single 'who am I' call right after authentication.","responses":{"200":{"description":"Profile data.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","format":"email"},"name":{"type":"string","nullable":true},"preferredLanguage":{"type":"string","description":"ISO 639-1 UI language code."},"createdOn":{"type":"string","format":"date-time"},"directory":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"balance":{"type":"number"},"freeTranscriptionsRemaining":{"type":"integer"}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/transcriptions":{"post":{"tags":["Transcriptions"],"summary":"Create transcription (file upload)","description":"Upload an audio or video file for transcription. Returns immediately with status `queued`; poll GET /v1/transcriptions/{id}/status for progress, then GET /v1/transcriptions/{id} once status flips to `completed` for the full result.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"Audio/video file (MP3, WAV, FLAC, OGG, M4A, MP4, MKV, WebM, MOV). Max 500 MB."},"model":{"type":"string","enum":["standard","premium"],"default":"standard","description":"Standard: €0.003/min. Premium: €0.0045/min — DACH dialect optimised, priority queue, higher diarization accuracy."},"language":{"type":"string","default":"auto","description":"ISO 639-1 code or 'auto' for auto-detection."},"vocabulary":{"type":"string","description":"JSON array of custom words, or comma-separated string."},"vocabulary_id":{"type":"string","format":"uuid","description":"ID of a saved vocabulary."},"num_speakers":{"type":"integer","minimum":1,"maximum":50,"description":"Exact speaker count. Mutually exclusive with min/max."},"min_speakers":{"type":"integer","minimum":1,"maximum":50,"description":"Lower bound for speaker count. Must be paired with max_speakers."},"max_speakers":{"type":"integer","minimum":1,"maximum":50,"description":"Upper bound for speaker count. Must be paired with min_speakers."}}}}}},"responses":{"202":{"description":"Transcription queued. Subsequent polls reveal progress.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"model":{"type":"string","enum":["standard","premium"]},"originalFileName":{"type":"string"},"createdOn":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"`missing-file` — multipart body did not include a `file` field.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientBalance"},"422":{"description":"One of `unsupported-format`, `file-too-large`, `invalid-model`, `invalid-speaker-hint`. Discriminate on the response body's `type` slug.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"get":{"tags":["Transcriptions"],"summary":"List transcriptions","parameters":[{"name":"cursor","in":"query","schema":{"type":"string","format":"uuid"},"description":"Cursor returned by a previous page. Omit on the first call."},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}},{"name":"status","in":"query","schema":{"type":"string","enum":["queued","processing","completed","failed"]}},{"name":"model","in":"query","schema":{"type":"string","enum":["standard","premium"]}},{"name":"search","in":"query","schema":{"type":"string"},"description":"Substring search across filename and result text."}],"responses":{"200":{"description":"Paginated list.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Transcription"}},"cursor":{"type":"string","nullable":true,"description":"Pass back as the `cursor` query param to fetch the next page; null when no more results."},"hasMore":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/transcriptions/{id}":{"get":{"tags":["Transcriptions"],"summary":"Get transcription detail","description":"Returns the full transcription record including resultText, resultJson (word-level offsets + speakers), pricing and timestamps. Use this once a polling check on /status reports `completed` — the heavy `resultJson` is only worth fetching at the end.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Transcription detail with results.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TranscriptionDetail"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"delete":{"tags":["Transcriptions"],"summary":"Delete transcription","description":"Hard-deletes the transcription row and the underlying audio/video file from object storage. Idempotent — calling again returns 404.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/transcriptions/{id}/status":{"get":{"tags":["Transcriptions"],"summary":"Get transcription status (lightweight polling)","description":"Returns only id, status and progress. Roughly 10× cheaper than the detail endpoint — recommended polling interval: 2–5 seconds. Once status flips to `completed` or `failed`, fetch GET /v1/transcriptions/{id} for the full result.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Status snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","processing","completed","failed"]},"progress":{"type":"integer","minimum":0,"maximum":100}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/transcriptions/{id}/events":{"get":{"tags":["Transcriptions"],"summary":"SSE stream for real-time status updates","description":"Server-Sent Events stream emitting `{status, progress}` JSON frames every time either field changes. Stream closes after a terminal status (`completed` / `failed`).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"SSE stream.","content":{"text/event-stream":{}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/transcriptions/{id}/export":{"get":{"tags":["Transcriptions"],"summary":"Export transcription","description":"Returns the transcription rendered as the requested format. Only allowed for `completed` transcriptions.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","required":true,"schema":{"type":"string","enum":["txt","srt","vtt","json"]}}],"responses":{"200":{"description":"File download with `Content-Disposition: attachment`.","content":{"text/plain":{},"text/srt":{},"text/vtt":{},"application/json":{}}},"400":{"description":"`not-ready` — transcription has not finished processing yet.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"`invalid-format` — `format` query is not one of txt/srt/vtt/json.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/vocabularies":{"get":{"tags":["Vocabularies"],"summary":"List vocabularies","responses":{"200":{"description":"Vocabulary list.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Vocabulary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"post":{"tags":["Vocabularies"],"summary":"Create vocabulary","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","words"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100},"words":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":500}}}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Vocabulary"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/vocabularies/{id}":{"get":{"tags":["Vocabularies"],"summary":"Get vocabulary","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Vocabulary detail.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Vocabulary"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"put":{"tags":["Vocabularies"],"summary":"Update vocabulary","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"words":{"type":"array","items":{"type":"string","minLength":1},"minItems":1,"maxItems":500}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Vocabulary"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"delete":{"tags":["Vocabularies"],"summary":"Delete vocabulary","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/webhooks":{"get":{"tags":["Webhooks"],"summary":"List webhook endpoints","responses":{"200":{"description":"Webhook list (secret is omitted; it's only returned on creation).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"isActive":{"type":"boolean"},"createdOn":{"type":"string","format":"date-time"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"post":{"tags":["Webhooks"],"summary":"Create webhook endpoint","description":"The webhook secret is returned only once upon creation. Store it securely — there is no recovery flow.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","minItems":1,"items":{"type":"string","enum":["transcription.completed","transcription.failed","balance.low"]}}}}}}},"responses":{"201":{"description":"Created. The `secret` is only returned in this response — store it now.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"secret":{"type":"string","description":"HMAC signing secret. Returned ONCE."},"isActive":{"type":"boolean"},"createdOn":{"type":"string","format":"date-time"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/v1/webhooks/{id}":{"delete":{"tags":["Webhooks"],"summary":"Delete webhook endpoint","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimitExceeded"},"500":{"$ref":"#/components/responses/InternalServerError"}}}}},"webhooks":{"transcription.completed":{"post":{"tags":["Webhooks"],"summary":"Sent when a transcription completes","description":"Fires once per transcription as soon as the engine returns a final result. `data` mirrors the `Transcription` shape with the fields known at completion time. Your endpoint must respond 2xx within 10s.","parameters":[{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptionCompletedPayload"},"example":{"event":"transcription.completed","timestamp":"1731340800","data":{"id":"f3a1c5b2-9c0e-4a1d-9f4f-7e0e8e6e6c11","model":"standard","status":"completed","duration":312.4,"wordCount":1543,"speakerCount":3,"language":"de","cost":0.94,"completedAt":"2026-05-14T10:00:00.000Z"}}}}},"responses":{"200":{"description":"Delivery acknowledged."},"4xx":{"description":"Non-retryable failure — delivery dropped."},"5xx":{"description":"Retryable failure — up to 3 attempts with linear backoff (5s, 10s)."}}}},"transcription.failed":{"post":{"tags":["Webhooks"],"summary":"Sent when a transcription fails","description":"Fires when the engine returns an unrecoverable error or the job exceeds the max retry budget. `errorMessage` carries the reason.","parameters":[{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranscriptionFailedPayload"},"example":{"event":"transcription.failed","timestamp":"1731340933","data":{"id":"f3a1c5b2-9c0e-4a1d-9f4f-7e0e8e6e6c11","model":"premium","status":"failed","errorMessage":"Audio decoding failed: unsupported codec.","failedAt":"2026-05-14T10:02:13.000Z"}}}}},"responses":{"200":{"description":"Delivery acknowledged."},"5xx":{"description":"Retryable failure — up to 3 attempts with linear backoff."}}}},"balance.low":{"post":{"tags":["Webhooks"],"summary":"Sent when workspace balance drops below the configured threshold","description":"Fires once per crossing of the configured threshold. To re-arm, the balance must recover above the threshold (e.g. via top-up) and drop again.","parameters":[{"$ref":"#/components/parameters/WebhookTimestamp"},{"$ref":"#/components/parameters/WebhookSignature"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BalanceLowPayload"},"example":{"event":"balance.low","timestamp":"1731341100","data":{"balance":0.42,"threshold":1,"currency":"EUR"}}}}},"responses":{"200":{"description":"Delivery acknowledged."},"5xx":{"description":"Retryable failure — up to 3 attempts with linear backoff."}}}}},"tags":[{"name":"System","description":"Health checks and system information"},{"name":"Account","description":"Balance, usage analytics, and profile"},{"name":"Transcriptions","description":"Create, manage, and export transcriptions"},{"name":"Vocabularies","description":"Manage custom word lists for better accuracy"},{"name":"Webhooks","description":"Configure webhook endpoints for event notifications"}]}