API
API endpoints
Every route with its parameters, responses and status codes. Create jobs, follow them, upload files, ask questions, read your account and your keys.
On this page
Base URL https://scribiz.com/api. Every path below is relative to it: POST /v1/context is POST https://scribiz.com/api/v1/context. Send Authorization: Bearer <key> on every call except /health, /v1/status, /v1/openapi.json and the anonymous web tool. For the shape of errors, see Errors and limits.
Jobs
POST /v1/context
Create a job that builds the Context of a link or an upload. The call checks the request, resolves the link, quotes the cost and checks your plan, then queues the job. Nothing is queued if any step refuses.
The body is JSON, up to 64 KB, and takes only the fields below. Send exactly one of url and upload_id.
| Field | Type | Description |
|---|---|---|
url | string | A public video or audio link, up to 2,048 characters. |
upload_id | string | An id from PUT /v1/uploads. |
mode | string | auto (default), captions, audio, visual or full. listen, watch and both are aliases for audio, visual and full. See Overview. |
language | string or array | A BCP-47 language hint, such as en or pt-BR. Up to 5 in an array. |
options | object | Optional settings, below. |
turnstile_token | string | A Cloudflare Turnstile token. Anonymous Listen and Watch need one when the server has Turnstile on. |
options field | Type | Description |
|---|---|---|
detail | transcript, brief, full | How much to write besides the transcript. Default full. |
diarize | boolean or "auto" | Separate the speakers. Default "auto". |
proofread | boolean, "terms", "full" or "auto" | How much to correct. |
include_words | boolean | Keep the word list in the result. Accounts only. |
quality | standard or high | Accounts only. |
visual | object | fps from 0.1 to 2 and max_scenes from 1 to 200. Accounts only. |
vocabulary | array of strings | Up to 50 names and terms, 100 characters each. Accounts only. |
An anonymous request that sets an accounts-only option is 403 with account_required.
| Header | Description |
|---|---|
Authorization | Bearer <key> |
Idempotency-Key | Optional. A repeated request with the same key and body returns the same job. |
curl https://scribiz.com/api/v1/context \ -H "Authorization: Bearer $SCRIBIZ_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \ -d '{"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw", "mode": "auto"}'HTTP/1.1 202 AcceptedLocation: /v1/jobs/job_2DUafpGltJTqVKlAYlIREmContent-Type: application/json{ "estimate": { "cached": false, "charges_as": "captions", "media_seconds": 19, "minutes": 0.031667, "tiers": ["captions", "synthesis"] }, "events_url": "/api/v1/jobs/job_2DUafpGltJTqVKlAYlIREm/events", "job_id": "job_2DUafpGltJTqVKlAYlIREm", "mode": "auto", "queue_position": 1, "result_url": "/api/v1/jobs/job_2DUafpGltJTqVKlAYlIREm", "status": "queued"}| Field | Description |
|---|---|
job_id | The job id. |
status | queued. |
mode | The mode you asked for. |
estimate | The quote. minutes is what the job charges if it runs as quoted, and 0 when cached is true. charges_as is captions, audio, visual or full. media_seconds is the length of the source. tiers lists the steps that will run. |
events_url, result_url | Paths on the host you called, under the prefix you used. Called at https://scribiz.com/api/v1/context, they start with /api/v1: join them to https://scribiz.com, not to the base. |
queue_position | How many jobs are ahead. |
degraded | Present when Auto was switched to captions because the free Listen and Watch budget is spent: { "from": "auto", "to": "captions", "reason": "busy" }. |
idempotent_replay | true when this is the job of an earlier request with the same Idempotency-Key. The response also has the header Idempotent-Replayed: true. |
upload_id | Only from POST /v1/transcribe. |
A cached result still goes through a job and finishes almost at once. A request that could not be accepted fails before a job exists:
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body is not valid. The message says what is wrong. |
| 401 | invalid_api_key, unauthorized | The key is not valid |
| 402 | quota_exceeded | Not enough minutes. Carries minutesNeeded and minutesRemaining. |
| 403 | account_required, turnstile_required, turnstile_failed, insufficient_scope | The caller may not do this |
| 404 | upload_not_found | The upload id is not yours or does not exist |
| 409 | upload_in_use | The upload already belongs to another job |
| 410 | upload_expired | The upload is more than an hour old |
| 413 | video_too_long | The video is over your plan's limit |
| 415 | unsupported_media_type | The upload cannot be read |
| 422 | url_unsupported, source_unavailable, source_live, source_auth_required and the other source errors | The link cannot be read. Carries engineCode and a hint. |
| 429 | anonymous_limit, too_many_in_flight, rate_limited | A limit. Honor Retry-After. |
| 451 | blocked_content | The video was removed after a takedown request |
| 503 | busy, not_configured | The queue is full, the disk is low, or Listen and Watch are paused for anonymous use |
A link that needs a login, such as an Instagram post, is refused here with source_auth_required, not reported later on the job. See Errors and limits.
GET /v1/jobs/:id
The state of a job. When it succeeded, result is the Context.
{ "attempt": 1, "cache_hit": null, "cancel_requested": false, "created_at": 1791094927185, "error": null, "estimate": { "cached": false, "charges_as": "audio", "media_seconds": 12.120726, "minutes": 0.202012, "tiers": ["audio", "synthesis"] }, "events_url": "/api/v1/jobs/job_7b9rgMRwWjy1mIoS64A8CU/events", "expires_at": 1793686927185, "finished_at": null, "input": { "kind": "upload", "upload_id": "upl_1MOSdD3kCZtK72deTv7Dw6" }, "job_id": "job_7b9rgMRwWjy1mIoS64A8CU", "minutes_charged": null, "mode": "audio", "progress": 0.88, "result": null, "result_url": "/api/v1/jobs/job_7b9rgMRwWjy1mIoS64A8CU", "stage": "synthesize", "started_at": 1791094927185, "status": "running"}| Field | Type | Description |
|---|---|---|
job_id | string | The job id. |
status | string | queued, running, succeeded, failed or canceled. |
mode | string | The mode as requested. |
input | object | { "kind": "url", "url": ... } or { "kind": "upload", "upload_id": ... }. A link is shown without credentials or a signed query. |
stage | string or null | The current step: resolve, download, extract, transcribe, visual, synthesize and the others listed in The JSON protocol. |
progress | number or null | 0 to 1, never going backwards. A rough guide, not a promise. |
estimate | object | The quote from creation. |
cache_hit | boolean or null | Whether the job was served from stored results. Null until it ends. |
minutes_charged | number or null | Minutes charged. Null until the job ends, and null for a job that failed or was canceled. |
degraded | object | As on creation, when present. |
result | Context or null | The Context, when status is succeeded. |
error | object or null | The engine's error, when status is failed or canceled. See The error object. |
attempt | number | How many times the job was started. A job interrupted by a restart runs again, up to three times. |
cancel_requested | boolean | Whether DELETE was called while it ran. |
created_at, started_at, finished_at, expires_at | number or null | Milliseconds since the Unix epoch. Results are deleted at expires_at: 24 hours after creation for anonymous jobs, 30 days for accounts. |
events_url, result_url | string | Where to stream and where to read. |
A job that belongs to another account is 404 with not_found, the same as one that does not exist. An expired job is 410 with job_expired. A recording with no speech, such as music, is a normal succeeded job with result.transcript set to null and the warning NO_SPEECH_DETECTED.
GET /v1/jobs/:id/events
A stream of server-sent events for one job, for a live progress display.
curl -N https://scribiz.com/api/v1/jobs/job_2DUafpGltJTqVKlAYlIREm/events \ -H "Authorization: Bearer $SCRIBIZ_API_KEY"- Each event is three lines:
id: <number>,event: <type>anddata: <JSON>. The data is one object. These are the same objects as in The JSON protocol:start,plan,stage,progress,chunk,retry,warning,partial,usage,cache,doneanderror. The API addsqueued,started(once per attempt) andrequeued. - The stream starts with
retry: 3000, and then replays every stored event. - Reconnect with a
Last-Event-IDheader, or?last_event_id=, and the stream replays what you missed, then continues. - A
: pingline, a comment, is sent every 15 seconds. - The stream ends after exactly one
doneor oneerror. A canceled job ends with anerrorwhose code isCANCELLED. progressandpartialevents are thinned to about one every 400 milliseconds per step. A job keeps at most 2,000 events.- An owner can hold 8 streams open at once. More is
429withtoo_many_streams. - A
partialevent says how much of the video the transcript covers so far, so a page can show text while the rest is still running.
A stream that drops is not a failed job. Read GET /v1/jobs/:id to find out.
DELETE /v1/jobs/:id
Cancel a running job, or delete the stored result of a finished one. Only the account or anonymous caller that created the job can do it. Anyone else gets 403 with not_owner.
curl -X DELETE https://scribiz.com/api/v1/jobs/job_0fVFR5QoOu2P2t3QkDkj2H \ -H "Authorization: Bearer $SCRIBIZ_API_KEY"- A queued job is canceled at once and answers
200. - A running job is aborted. The call waits up to 10 seconds and answers
200with the final job, whosestatusiscanceledand whoseerror.codeisCANCELLED. If it is still winding down, the answer is202. Nothing is charged for a canceled job. - A finished job has its result erased and answers
200with{ "job_id": "...", "status": "deleted" }. A laterGETanswers404.
Uploads
PUT /v1/uploads
Upload a media file to use in a job. The request body is the file itself, not JSON and not multipart. It takes audio and video files.
| Header | Description |
|---|---|
Content-Type | The type of the file, for example audio/mpeg, audio/mp4, audio/wav or video/mp4. |
Content-Length | Required. The body is streamed and capped at 100 MB. |
X-File-Name | The original file name, URL-encoded. |
{ "bytes": 73264, "duration_seconds": 12.120726, "expires_at": 1791097083297, "file_name": "speech.mp3", "has_audio": true, "has_video": false, "mime": "audio/mpeg", "upload_id": "upl_4dEjVvJnlzplZvFcA2PULr"}ffprobe must name a real media container. Scribiz decides that from the file, not from the Content-Type or the extension. A playlist or a text file is refused with 415. An upload is kept for one hour. Pass its id to POST /v1/context in that time. The file belongs to one job: it is removed when the job succeeds, and kept for a retry when it fails.
| Status | Code | When |
|---|---|---|
| 400 | length_mismatch | The body was not as long as Content-Length said |
| 411 | length_required | No Content-Length |
| 413 | body_too_large | The file is over 100 MB. Checked before the body is read. |
| 415 | unsupported_media | Not a media file Scribiz can read |
| 429 | too_many_in_flight, too_many_uploads | Too many uploads at once, or waiting to be used (anonymous 1 at a time and 3 waiting, accounts 2 and 10) |
| 503 | busy | The disk is low |
POST /v1/transcribe
A shortcut that uploads a file and creates a job in one request. It takes a multipart/form-data body with the file in file, and optional mode, language, options (a JSON string) and turnstile_token. It has the same 100 MB cap, and answers like POST /v1/context plus upload_id. If the job is refused after the file arrived, the error carries upload_id, so you can call POST /v1/context without sending the file again.
curl https://scribiz.com/api/v1/transcribe \ -H "Authorization: Bearer $SCRIBIZ_API_KEY" \ -F "[email protected]" \ -F "mode=audio"The server buffers a multipart body in memory, about three times the size of the file, and parses one at a time. Prefer PUT /v1/uploads.
Questions
POST /v1/ask
Ask a question about a Context you already have. It costs 0.1 minutes.
| Field | Type | Description |
|---|---|---|
job_id | string | A job of yours that has succeeded. Give this or context. |
context | object | A Context, at most 1 MB. Give this or job_id. |
question | string | 1 to 8,000 characters. |
{ "answer": "The speaker mentions that the cool thing about elephants is that they have really, really long trunks, and adds that that is pretty much all there is to say about them.", "citations": [ { "start": 5.318, "end": 14.367, "text": "the cool thing about these guys is that they have really... really really long trunks and that's cool" }, { "start": 16.881, "end": 18.881, "text": "and that's pretty much all there is to say" } ], "minutes_charged": 0.1}citations point at real transcript segments. This route never starts the on-screen layer: for a question about the picture on a Context that has none, notes says so. A job that has not succeeded is 409 with job_not_ready. Anonymous callers get 5 questions a day, then 429 with anonymous_limit.
Account
GET /v1/me
The account behind the key, its plan and its minutes. Without a credential it describes the anonymous limits instead.
{ "anonymous": false, "auth": { "keyId": "key_GDAeFMIBQ2lh", "scopes": "all", "via": "api_key" }, "entitlement": { "lifetimeMac": false, "minutesIncluded": 30, "minutesTopup": 0, "period": { "end": 1793491200000, "key": "2026-10-01", "start": 1790812800000 }, "plan": "free", "planUntil": null, "source": "none" }, "limits": { "apiKeys": { "active": 1, "max": 10 }, "maxConcurrentJobs": 2, "maxVideoSeconds": 7200, "minutesPerPeriod": 30, "proxy": { "maxInflightCalls": 4, "requestsPerMinute": 60, "uploadBytesPerDay": 1073741824 } }, "plan": "free", "usage": { "audioSeconds": 0, "costMicros": 0, "includedRemaining": 30, "minutesRemaining": 30, "minutesTopup": 0, "minutesUsed": 0, "period": "2026-10-01", "periodEnd": 1793491200000, "periodStart": 1790812800000 }, "user": { "email": "[email protected]", "id": "xEPzDpPvurJRuQneA7TXzN9ldow7Xo25", "name": "" }}| Field | Description |
|---|---|
plan | free, pro or lifetime, or anonymous |
auth | How the call was authenticated: via is api_key for a key, bearer for a session token or session for a browser session, with the scopes and the keyId of a key |
entitlement.minutesIncluded | Minutes the plan gives for the current period |
entitlement.minutesTopup | Top-up minutes. They do not expire. |
entitlement.period | The current period. Free and Pro minutes reset when it ends. |
entitlement.lifetimeMac | Whether the account has the Mac Lifetime license |
usage.minutesUsed | Minutes used in the current period |
usage.minutesRemaining | Included minutes left plus top-up minutes |
limits | The limits of the plan: video length, running jobs, keys |
For an anonymous caller, usage has day, minutesUsed, minutesRemaining, captionJobsUsed, captionJobsRemaining and resetsAt, and limits has minutesPerDay, captionJobsPerDay, asksPerDay, maxVideoSeconds, maxConcurrentJobs and resultTtlHours.
Keys
GET /v1/keys
List your keys. The key itself is never returned again, only its prefix and its last four characters. It works with a key that has the all scope. See API authentication for the response.
POST /v1/keys
Create a key. It needs a signed-in session, which the dashboard has. A key cannot call it: that is 403 with session_required. The response is the only time you see the key.
| Field | Type | Description |
|---|---|---|
name | string | A label, such as the service that will use the key. |
scopes | string | all, read or mcp. Default all. |
expiresInDays | number | Optional. The key stops working after this many days. |
{ "createdAt": 1791093434663, "createdVia": "dashboard", "deviceLabel": null, "expiresAt": null, "id": "key_T09PW9orWbQL", "key": "sbz_live_your_new_key_here", "lastUsedAt": null, "last4": "UYZC", "name": "mcp agent", "prefix": "sbz_live_", "scopes": "mcp"}An account can hold up to 10 active keys. The next is 409 with key_limit_reached.
DELETE /v1/keys/:id
Revoke a key. It needs a signed-in session. The key stops working at once, and everywhere within about a minute. A key that is not yours is 404.
Other routes
GET /v1/status
How the service is doing, for a status page. No key needed. The answer is cached for 10 seconds.
{ "free_tier": { "listen_watch": "open" }, "ok": true, "queue": { "capacity": 2, "queued": 0, "running": 0 }, "sources": [ { "avg_seconds": 8.2, "blocked": 0, "failed": 0, "jobs": 1, "source": "youtube", "succeeded": 1, "success_rate": 1 } ], "status": "operational", "time": 1791093526634, "window_hours": 24}sources covers the last 24 hours. A failure that is the caller's, such as a private video or no minutes, does not count against a source. free_tier.listen_watch is paused when Listen and Watch are switched off for anonymous callers. status is degraded then.
GET /v1/openapi.json
The OpenAPI 3.1 description of the API. No key needed.
GET /health
Returns {"ok":true} when the service is up. It reports nothing else.
POST /v1/billing/checkout
Starts a purchase and answers where to send the buyer: { "url", "sessionId", "expiresAt", "amount", "currency", "earlyBird" }. url is a Stripe payment page, and amount (in cents, before tax) is what that page charges. Send plan (pro, mac-lifetime or topup), with interval for Pro and pack for a top-up, and never a price. It needs a signed-in session, not an API key. While nothing is on sale it answers 501 with the code billing_not_configured.
DELETE /v1/account
Delete your account and everything it owns: results, keys and files at Google. It needs a signed-in session.
/mcp
The remote MCP server. It speaks the Streamable HTTP transport and takes the same API keys. It also answers without a key, inside a small daily allowance. See MCP.
Routes that are not part of the API
These exist for Scribiz's own apps. Do not build on them. They can change without notice.
POST /v1/cli/exchangeswaps a browser approval for an API key. It is the last step of command-line sign-in (scribiz login), and the key it returns is the one the CLI saves./v1/gemini/*is a metered, allow-listed route that the CLI uses when it is signed in to a Scribiz account. Usage is measured from what the model reports, not from anything the caller says.https://scribiz.com/api/auth/*is sign-in for the website and the device flow. The code you confirm is entered athttps://scribiz.com/login/device.
Checked against the Scribiz build on 2026-10-04.