Skip to content
Try it free

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.

View as Markdown
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.

FieldTypeDescription
urlstringA public video or audio link, up to 2,048 characters.
upload_idstringAn id from PUT /v1/uploads.
modestringauto (default), captions, audio, visual or full. listen, watch and both are aliases for audio, visual and full. See Overview.
languagestring or arrayA BCP-47 language hint, such as en or pt-BR. Up to 5 in an array.
optionsobjectOptional settings, below.
turnstile_tokenstringA Cloudflare Turnstile token. Anonymous Listen and Watch need one when the server has Turnstile on.
options fieldTypeDescription
detailtranscript, brief, fullHow much to write besides the transcript. Default full.
diarizeboolean or "auto"Separate the speakers. Default "auto".
proofreadboolean, "terms", "full" or "auto"How much to correct.
include_wordsbooleanKeep the word list in the result. Accounts only.
qualitystandard or highAccounts only.
visualobjectfps from 0.1 to 2 and max_scenes from 1 to 200. Accounts only.
vocabularyarray of stringsUp to 50 names and terms, 100 characters each. Accounts only.

An anonymous request that sets an accounts-only option is 403 with account_required.

HeaderDescription
AuthorizationBearer <key>
Idempotency-KeyOptional. A repeated request with the same key and body returns the same job.
Terminal
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"}'
202 Accepted
HTTP/1.1 202 AcceptedLocation: /v1/jobs/job_2DUafpGltJTqVKlAYlIREmContent-Type: application/json
Body
{  "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"}
FieldDescription
job_idThe job id.
statusqueued.
modeThe mode you asked for.
estimateThe 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_urlPaths 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_positionHow many jobs are ahead.
degradedPresent when Auto was switched to captions because the free Listen and Watch budget is spent: { "from": "auto", "to": "captions", "reason": "busy" }.
idempotent_replaytrue when this is the job of an earlier request with the same Idempotency-Key. The response also has the header Idempotent-Replayed: true.
upload_idOnly 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:

StatusCodeWhen
400invalid_requestThe body is not valid. The message says what is wrong.
401invalid_api_key, unauthorizedThe key is not valid
402quota_exceededNot enough minutes. Carries minutesNeeded and minutesRemaining.
403account_required, turnstile_required, turnstile_failed, insufficient_scopeThe caller may not do this
404upload_not_foundThe upload id is not yours or does not exist
409upload_in_useThe upload already belongs to another job
410upload_expiredThe upload is more than an hour old
413video_too_longThe video is over your plan's limit
415unsupported_media_typeThe upload cannot be read
422url_unsupported, source_unavailable, source_live, source_auth_required and the other source errorsThe link cannot be read. Carries engineCode and a hint.
429anonymous_limit, too_many_in_flight, rate_limitedA limit. Honor Retry-After.
451blocked_contentThe video was removed after a takedown request
503busy, not_configuredThe 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.

200 OK, while running
{  "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"}
FieldTypeDescription
job_idstringThe job id.
statusstringqueued, running, succeeded, failed or canceled.
modestringThe mode as requested.
inputobject{ "kind": "url", "url": ... } or { "kind": "upload", "upload_id": ... }. A link is shown without credentials or a signed query.
stagestring or nullThe current step: resolve, download, extract, transcribe, visual, synthesize and the others listed in The JSON protocol.
progressnumber or null0 to 1, never going backwards. A rough guide, not a promise.
estimateobjectThe quote from creation.
cache_hitboolean or nullWhether the job was served from stored results. Null until it ends.
minutes_chargednumber or nullMinutes charged. Null until the job ends, and null for a job that failed or was canceled.
degradedobjectAs on creation, when present.
resultContext or nullThe Context, when status is succeeded.
errorobject or nullThe engine's error, when status is failed or canceled. See The error object.
attemptnumberHow many times the job was started. A job interrupted by a restart runs again, up to three times.
cancel_requestedbooleanWhether DELETE was called while it ran.
created_at, started_at, finished_at, expires_atnumber or nullMilliseconds since the Unix epoch. Results are deleted at expires_at: 24 hours after creation for anonymous jobs, 30 days for accounts.
events_url, result_urlstringWhere 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.

Terminal
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> and data: <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, done and error. The API adds queued, started (once per attempt) and requeued.
  • The stream starts with retry: 3000, and then replays every stored event.
  • Reconnect with a Last-Event-ID header, or ?last_event_id=, and the stream replays what you missed, then continues.
  • A : ping line, a comment, is sent every 15 seconds.
  • The stream ends after exactly one done or one error. A canceled job ends with an error whose code is CANCELLED.
  • progress and partial events 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 429 with too_many_streams.
  • A partial event 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.

Terminal
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 200 with the final job, whose status is canceled and whose error.code is CANCELLED. If it is still winding down, the answer is 202. Nothing is charged for a canceled job.
  • A finished job has its result erased and answers 200 with { "job_id": "...", "status": "deleted" }. A later GET answers 404.

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.

HeaderDescription
Content-TypeThe type of the file, for example audio/mpeg, audio/mp4, audio/wav or video/mp4.
Content-LengthRequired. The body is streamed and capped at 100 MB.
X-File-NameThe original file name, URL-encoded.
201 Created
{  "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.

StatusCodeWhen
400length_mismatchThe body was not as long as Content-Length said
411length_requiredNo Content-Length
413body_too_largeThe file is over 100 MB. Checked before the body is read.
415unsupported_mediaNot a media file Scribiz can read
429too_many_in_flight, too_many_uploadsToo many uploads at once, or waiting to be used (anonymous 1 at a time and 3 waiting, accounts 2 and 10)
503busyThe 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.

Terminal
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.

FieldTypeDescription
job_idstringA job of yours that has succeeded. Give this or context.
contextobjectA Context, at most 1 MB. Give this or job_id.
questionstring1 to 8,000 characters.
200 OK
{  "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.

200 OK, with a key
{  "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": "" }}
FieldDescription
planfree, pro or lifetime, or anonymous
authHow 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.minutesIncludedMinutes the plan gives for the current period
entitlement.minutesTopupTop-up minutes. They do not expire.
entitlement.periodThe current period. Free and Pro minutes reset when it ends.
entitlement.lifetimeMacWhether the account has the Mac Lifetime license
usage.minutesUsedMinutes used in the current period
usage.minutesRemainingIncluded minutes left plus top-up minutes
limitsThe 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.

FieldTypeDescription
namestringA label, such as the service that will use the key.
scopesstringall, read or mcp. Default all.
expiresInDaysnumberOptional. The key stops working after this many days.
201 Created
{  "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.

200 OK
{  "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/exchange swaps 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 at https://scribiz.com/login/device.

Checked against the Scribiz build on 2026-10-04.

Loading the index