CLI
The JSON protocol
The newline-delimited JSON the CLI prints with --json, so you can run it from your own tool.
On this page
With --json, the CLI prints one JSON object per line on standard output and nothing else. Logs go to standard error. This is the contract a host application uses to run the CLI. It is versioned, and it is the way to put Scribiz inside your own tool.
scribiz speech.mp3 --json --out-file result.jsonRules:
- Every line is one complete JSON object in UTF-8, ended by
\n. Parse line by line. The CLI escapes U+2028, U+2029 and U+0085 inside strings, so any line splitter agrees where a line ends. - Standard output is flushed after every line.
- Every object has a string
type. Key order inside an object is not defined. - Ignore message types and fields you do not know. That is how the protocol grows without breaking you.
--jsonimplies--no-input. The CLI never prompts. A missing credential is anerrorline and exit code 3.- Secrets arrive through the environment (
SCRIBIZ_API_KEY,GEMINI_API_KEY, orSCRIBIZ_CONFIG_DIRto point at a saved credential), never as arguments. - If your side closes the pipe, the CLI stops and exits 0 without a stack trace.
- The process is one job. It starts, does the work, writes one last line (
resultorerror) and exits.
A whole run
Each run starts with hello and ends with result or error. This is a real run on speech.mp3, a 12 second recording, with the repeated progress lines and the file path shortened:
{"capabilities":["captions","audio","visual","ask","format","login","cancel","out-file"],"cli":"0.1.1","engine":"0.1.0","mediaPrep":["ffmpeg"],"pid":14185,"protocol":1,"type":"hello","command":"transcribe"}{"input":"/Users/you/speech.mp3","mode":"auto","type":"start","runId":"a5912da2","t":0}{"stage":"resolve","status":"start","type":"stage","runId":"a5912da2","t":0}{"detail":"local:8ee9f0a250ac5a75f42640c01fdbfc97","ms":38,"stage":"resolve","status":"done","type":"stage","runId":"a5912da2","t":38}{"estimate":{"breakdown":{"audio":{"usdHigh":0.001396,"usdLow":0.001032},"synthesis":{"usdHigh":0.008546,"usdLow":0.001708}},"durationSeconds":12.120726,"minutes":0.2,"pricedAt":"2026-10-04","tiers":["audio","synthesis"],"usdHigh":0.009942,"usdLow":0.00274},"reason":"Listening to the audio.","tiers":["audio","synthesis"],"type":"plan","runId":"a5912da2","t":40}{"stage":"extract","status":"start","type":"stage","runId":"a5912da2","t":42}{"fraction":1,"stage":"extract","type":"progress","runId":"a5912da2","t":103}{"ms":63,"stage":"extract","status":"done","type":"stage","runId":"a5912da2","t":105}{"detail":"1 chunk, 9 s of speech","ms":15,"stage":"chunk","status":"done","type":"stage","runId":"a5912da2","t":120}{"attempt":1,"durationSeconds":12.121,"index":0,"startSeconds":0,"status":"uploading","total":1,"type":"chunk","runId":"a5912da2","t":167}{"attempt":1,"durationSeconds":12.121,"index":0,"startSeconds":0,"status":"transcribing","total":1,"type":"chunk","runId":"a5912da2","t":1739}{"delta":{"audioSeconds":12.16,"model":"gemini-3.5-transcribe","tokens":{"audioIn":304,"in":305,"out":40,"thought":0,"videoIn":0},"usd":0.00109,"videoSeconds":0,"stage":"transcribe"},"total":{"audioSeconds":12.16,"tokens":{"audioIn":304,"in":305,"out":40,"thought":0,"videoIn":0},"usdEstimate":0.00109,"videoSeconds":0},"type":"usage","runId":"a5912da2","t":3470}{"coveredSeconds":12.121,"type":"partial","words":31,"runId":"a5912da2","t":3472}{"detail":"0 of 1 chunks need repair","ms":1,"stage":"verify","status":"done","type":"stage","runId":"a5912da2","t":3473}{"t":5002,"type":"heartbeat"}{"ms":2615,"stage":"synthesize","status":"done","type":"stage","runId":"a5912da2","t":6094}{"summary":{"durationSeconds":12.120726,"ms":6095,"usd":0.0032702499999999997,"words":31},"type":"done","runId":"a5912da2","t":6095}{"type":"result","bytes":5009,"kind":"context","path":"/Users/you/result.json"}A bare scribiz <input> --json builds the whole Context, so a run ends with the summary step, as above.
Lines you will see
Every event from the engine carries runId (a short id for the run) and t (milliseconds since the run started). The lines the CLI adds itself (hello, device_code, heartbeat, item, summary, result and the media requests) do not carry runId.
type | When | Fields |
|---|---|---|
hello | First line, always | protocol (always 1 today), engine and cli (versions), pid, command, capabilities, mediaPrep |
start | The run begins | input, mode |
plan | After the source is resolved, and again if the plan changes | tiers, reason, estimate (minutes, usdLow, usdHigh, durationSeconds, pricedAt, breakdown) |
stage | A step starts, ends, is skipped or fails | stage, status (start, done, skipped, failed), detail, ms |
progress | A step reports how far it is | stage, fraction (0 to 1, or null), done, total, unit, etaSeconds |
chunk | One piece of audio changes state | index, total, status, attempt, startSeconds, durationSeconds, words |
retry | A request failed and will be tried again | stage, attempt, maxAttempts, delayMs, code, message |
warning | Something is approximate or incomplete | code, message, data |
partial | A growing transcript is available | coveredSeconds, words |
usage | A model call finished | delta, total |
cache | A stored layer was looked up | layer (meta, captions, transcript, visual, synthesis), hit |
done | The work finished | summary (words, durationSeconds, usd, ms) |
heartbeat | Every 5 seconds from hello to the last line | t (milliseconds since hello) |
device_code | scribiz login --json, once the code is issued | userCode, verificationUri, verificationUriComplete, expiresIn (seconds), interval (seconds) |
item | Before each input of a batch | index, input, total |
summary | Last line of a batch | total, succeeded, failed, stopped |
result | The work is ready | kind, then the fields for that kind |
error | The run failed | error |
A run has exactly one start, then exactly one done or one error. The stages are resolve, download, extract, chunk, upload, transcribe, verify, repair, proofread, visual-prep, visual, synthesize and render. chunk statuses are queued, uploading, transcribing, verifying, retry, repairing, done and failed. In plan, tiers lists which of captions, audio, visual and synthesis will run, and it is empty when everything is stored. Warning codes are listed in The Context object.
progress, chunk and partial are for display. Do not derive results from them. The result is the Context.
A heartbeat tells you the process is alive while a long step runs. Three missed heartbeats, about 15 seconds with no line of any kind, is a reasonable point to call a run stalled.
The result
result is the last line of a successful run. Its kind says what it holds.
kind | Command | Fields |
|---|---|---|
context | scribiz <input>, scribiz context <input> | context, or path and bytes |
ask | scribiz ask | answer, citations |
login | scribiz login | saved, then configPath when it was saved, or key with --no-config. Also keyId and user (email) when known |
whoami | scribiz whoami | me |
doctor, jit | scribiz doctor | report, or jit |
format | scribiz format | format and text, or format, path and bytes |
setup, logout | scribiz setup, logout | configPath and verified, or removed (a list: Scribiz login, Gemini key) |
A context result has two shapes.
{"type":"result","bytes":5009,"kind":"context","path":"/Users/you/result.json"}{"type":"result","kind":"context","context":{"schema":1,"id":"local:8ee9f0a250ac5a75f42640c01fdbfc97"}}With --out-file, the CLI writes the Context there (mode 0600, atomically) and prints the path and size. Without it, a single input's Context is inline when it is at most 262,144 bytes. A larger one is written to a file under the system temp folder and announced by path, and the host deletes it. A two hour Context with word times is about 1 MB, so always pass --out-file when the input might be long.
A batch is a folder, or more than one input. Each result carries index. In a batch each Context is always written to a file: <name>.json in the --out-file folder, or, without --out-file, next to its source file (in the current folder for a link). The result line carries path and bytes, never context. Pass --out-file with a folder to keep the files out of your media folder.
The file holds the same Context as scribiz context --format json, with word times when the transcript has them. scribiz format and scribiz ask read it.
Signing in
scribiz login --json waits for a person to confirm a code. It prints hello, then device_code, then waits. Show userCode and open verificationUriComplete: the person confirms the code at https://scribiz.com/login/device while signed in to Scribiz. The CLI never opens a browser itself in this mode, and it never prompts.
{"capabilities":["captions","audio","visual","ask","format","login","cancel","out-file"],"cli":"0.1.1","engine":"0.1.0","mediaPrep":["ffmpeg"],"pid":14185,"protocol":1,"type":"hello","command":"login"}{"expiresIn":600,"interval":5,"type":"device_code","userCode":"ABCD1234","verificationUri":"https://scribiz.com/login/device","verificationUriComplete":"https://scribiz.com/login/device?user_code=ABCD1234"}{"type":"result","kind":"login","saved":true,"configPath":"/Users/you/.scribiz/config.json","keyId":"key_T09PW9orWbQL","user":{"email":"[email protected]"}}The code lasts expiresIn seconds. A code that expires or is denied ends the run with an error line (AUTH, exit code 3). With --no-config nothing is saved: the result carries the key instead of configPath, so your tool can keep it. Treat it like any secret.
Errors
{"type":"error","error":{"code":"AUTH","message":"No credential found","retryable":false,"scope":"account","stage":"resolve","hint":"Sign in with `scribiz login` (a free account), or use your own Gemini key with `scribiz setup`. In a script, set SCRIBIZ_API_KEY or GEMINI_API_KEY."}}An error line is the last line of a failed run. The engine's own error event is that line, so a run never prints two. Errors the CLI raises itself, such as a bad argument or a missing credential, have no runId or t. The process then exits with a code that matches the error:
| Code | Meaning |
|---|---|
| 0 | Done |
| 1 | Another failure |
| 2 | Wrong usage. The error code is INPUT_UNSUPPORTED. |
| 3 | Credentials or minutes |
| 4 | The source |
| 5 | The service |
| 6 | A tool is missing |
| 130 | Canceled |
error.retryable says whether trying again can help. error.scope is account for credential and minute problems, which stop a whole queue, or job for a problem with this one input. error.status and error.retryAfterMs carry the service's HTTP status and wait when there was one. Treat message and hint as untrusted text: they can hold part of a video's title or an upstream message. Every error code is described in Errors and limits.
In a batch, an error ends that item, not the process. The last line is a summary, with stopped: true when an account problem ended the remaining items. The exit code is the first failure's.
Cancel
Send SIGINT, SIGTERM or SIGHUP, or write a command to the CLI's standard input:
{"cmd":"cancel"}The CLI kills its child processes, deletes uploaded files and its run folder, prints an error with the code CANCELLED, and exits with 130.
{"type":"error","error":{"code":"CANCELLED","message":"Cancelled","retryable":false,"scope":"job","stage":"upload","hint":"The run was cancelled."},"runId":"dfc3f1f6","t":1465}A second signal, or a cancel that has not finished after 10 seconds, exits at once with 130 without cleaning up. Closing standard input does not cancel a run, except when you answer media requests (see below), where it means you are gone.
Let the host prepare media
An app that already knows how to read media, such as one built on AVFoundation, can do the media work and leave everything else to the CLI. Start it with --media-prep stdio (or stdio,ffmpeg to fall back to ffmpeg). Both need --json. The CLI then writes request lines and waits for responses on standard input.
{"type":"request","id":1,"op":"probe","args":{"path":"/Users/you/talk.mov"}}Answer with the same id. Several requests can be in flight at once, and answers can come in any order:
{"type":"response","id":1,"ok":true,"result":{"durationSeconds":252.4,"hasAudio":true,"hasVideo":true}}or fail it:
{"type":"response","id":1,"ok":false,"error":{"code":"UNSUPPORTED","message":"Cannot open this container"}}op | args | result |
|---|---|---|
probe | path | durationSeconds, hasAudio, hasVideo, and width, height, fps when known |
extractWav | path, out | durationSeconds. Write the whole audio as 16 kHz mono 16-bit PCM WAV to out, all tracks mixed. |
encodeChunk | wav, startSeconds, durationSeconds, out | path, mime. Optional. Encode that slice into a compact file such as AAC. Answer UNSUPPORTED and the CLI uploads WAV slices. |
makeVisualProxy | path, out, maxShortSide, fps, includeAudio | durationSeconds, bytes. A low-resolution H.264 MP4. The original video is never uploaded. |
While a long request runs, send {"type":"request-progress","id":2,"fraction":0.4}. It also restarts the request's timer. A request that goes quiet fails the run with TIMEOUT: after 60 seconds for probe, 120 for encodeChunk, and 10 minutes for extractWav and makeVisualProxy. The CLI then sends {"type":"request-cancel","id":2}, and you should stop that work and not answer.
The error codes of a response are UNSUPPORTED and FAILED. UNSUPPORTED means "not my job". With stdio,ffmpeg, it makes the CLI do that step with ffmpeg instead, and without ffmpeg the run stops with INPUT_UNSUPPORTED and a hint to install it. FAILED never falls back: for probe, extractWav and makeVisualProxy the run fails with INPUT_CORRUPT.
Whatever prepares the media, the results are identical: silence detection, cutting, merging and speaker mapping always run in the CLI on the audio.
Read it from code
import { spawn } from 'node:child_process'import { createInterface } from 'node:readline'const child = spawn('scribiz', ['https://youtu.be/jNQXAC9IVRw', '--json', '--out-file', 'result.json'], { stdio: ['pipe', 'pipe', 'inherit'],})for await (const line of createInterface({ input: child.stdout })) { const msg = JSON.parse(line) if (msg.type === 'hello' && msg.protocol !== 1) { child.stdin.write('{"cmd":"cancel"}\n') throw new Error(`Unsupported protocol ${msg.protocol}`) } if (msg.type === 'progress') { console.log(msg.stage, msg.fraction) } if (msg.type === 'result') { console.log('Context written to', msg.path) } if (msg.type === 'error') { console.error(msg.error.code, msg.error.message) }}// To cancel: child.stdin.write('{"cmd":"cancel"}\n')Schema
scribiz --json-schema prints one JSON document with a JSON Schema (draft 2020-12) for the Context, the progress events, the error object and every message on this page. Generate your types from it instead of writing them by hand, and decode a message by its type.
Checked against the Scribiz build on 2026-10-05.