# API quickstart

> Create a job, wait for it, and read the Context, in curl, TypeScript and Python. Then stream progress and upload a file.

Page: https://scribiz.com/docs/api/quickstart

You need an API key. Keys are made in the dashboard, after you sign in. See [API authentication](https://scribiz.com/docs/api/authentication.md). To check that the API is up first, run `curl https://scribiz.com/api/v1/status`. It needs no key.

```bash
export SCRIBIZ_API_KEY=sbz_live_your_key_here
```

The examples use a 19 second public YouTube video, so they cost almost nothing.

## Create a job and read the Context

```bash tab="curl"
JOB=$(curl -s https://scribiz.com/api/v1/context \
  -H "Authorization: Bearer $SCRIBIZ_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw", "mode": "auto"}' | jq -r .job_id)

while true; do
  STATE=$(curl -s "https://scribiz.com/api/v1/jobs/$JOB" -H "Authorization: Bearer $SCRIBIZ_API_KEY")
  STATUS=$(printf '%s' "$STATE" | jq -r .status)
  case "$STATUS" in
    succeeded) break ;;
    failed|canceled) printf '%s' "$STATE" | jq .error; exit 1 ;;
  esac
  sleep 3
done

printf '%s' "$STATE" | jq '.result.synthesis.summary.tldr'
```

```ts tab="TypeScript"
const API = 'https://scribiz.com/api'
const headers = {
  Authorization: `Bearer ${process.env.SCRIBIZ_API_KEY}`,
  'Content-Type': 'application/json',
}

async function getContext(url: string) {
  const create = await fetch(`${API}/v1/context`, {
    body: JSON.stringify({ mode: 'auto', url }),
    headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() },
    method: 'POST',
  })
  if (!create.ok) {
    throw new Error(`${create.status}: ${await create.text()}`)
  }
  const { job_id } = await create.json()

  while (true) {
    const res = await fetch(`${API}/v1/jobs/${job_id}`, { headers })
    const job = await res.json()
    if (job.status === 'succeeded') {
      return job.result
    }
    if (job.status === 'failed' || job.status === 'canceled') {
      throw new Error(job.error?.message ?? job.status)
    }
    await new Promise(resolve => setTimeout(resolve, 3000))
  }
}

const context = await getContext('https://www.youtube.com/watch?v=jNQXAC9IVRw')
console.log(context.synthesis?.summary.tldr)
```

```python tab="Python"
import os
import time
import uuid

import requests

API = "https://scribiz.com/api"
HEADERS = {"Authorization": f"Bearer {os.environ['SCRIBIZ_API_KEY']}"}


def get_context(url: str) -> dict:
    create = requests.post(
        f"{API}/v1/context",
        headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
        json={"url": url, "mode": "auto"},
        timeout=30,
    )
    create.raise_for_status()
    job_id = create.json()["job_id"]

    while True:
        job = requests.get(f"{API}/v1/jobs/{job_id}", headers=HEADERS, timeout=30).json()
        if job["status"] == "succeeded":
            return job["result"]
        if job["status"] in ("failed", "canceled"):
            raise RuntimeError(job.get("error") or job["status"])
        time.sleep(3)


context = get_context("https://www.youtube.com/watch?v=jNQXAC9IVRw")
print(context["synthesis"]["summary"]["tldr"])
```

```text title="Output"
The speaker stands in front of elephants and points out that they have very long trunks.
```

What you get back is the Context. Its fields are in [The Context object](https://scribiz.com/docs/api/context-object.md).

Three things the examples do on purpose:

- They send an `Idempotency-Key`, so a retry after a network error does not start a second job or charge twice.
- They poll every three seconds. A job that is still `queued` or `running` is not an error.
- They stop on `failed` and `canceled`, and show `error`, which has a `code` and a `hint`. See [Errors and limits](https://scribiz.com/docs/api/errors.md).

`result` is `null` while the job runs, and `synthesis` can be `null` if the summary step did not run, so check before you read into it. The `curl` example uses `printf '%s'` rather than `echo` on purpose: `echo` in zsh turns the `\n` inside the JSON into real line breaks, and `jq` then refuses the text.

## Watch progress

The job's event stream sends one event as the work happens. It is optional. `GET /v1/jobs/:id` is always enough.

```bash
curl -N "https://scribiz.com/api/v1/jobs/$JOB/events" \
  -H "Authorization: Bearer $SCRIBIZ_API_KEY"
```

```text title="Server-sent events"
retry: 3000

id: 1
event: queued
data: {"runId":"job_2DUafpGltJTqVKlAYlIREm","t":0,"type":"queued"}

id: 2
event: started
data: {"attempt":1,"runId":"job_2DUafpGltJTqVKlAYlIREm","t":0,"type":"started"}

id: 3
event: start
data: {"input":"https://www.youtube.com/watch?v=jNQXAC9IVRw","mode":"auto","type":"start","runId":"d6c4e6fe","t":0}

id: 11
event: stage
data: {"detail":"manual en captions, 6 segments","ms":5472,"stage":"download","status":"done","type":"stage","runId":"d6c4e6fe","t":5476}
...
id: 17
event: done
data: {"summary":{"durationSeconds":19,"ms":8197,"usd":0.0017175,"words":39},"type":"done","runId":"d6c4e6fe","t":8197}
```

Each `data:` line is a JSON event, the same ones the CLI prints with `--json`, plus `queued`, `started` and `requeued` from the API. See [The JSON protocol](https://scribiz.com/docs/cli/json-protocol.md). The `id:` lets you resume: reconnect with a `Last-Event-ID` header, or `?last_event_id=`, and the stream continues after that event. A line that starts with `:` is a keep-alive. The stream ends after the job is done or has failed.

## Upload a file

Upload the file first, then create the job from the upload. The API takes audio and video files, up to 100 MB. A video upload sends the whole file to Scribiz. To send only the sound, extract the audio first:

```bash
ffmpeg -i talk.mp4 -vn -ac 1 -ar 16000 -b:a 48k talk.mp3
```

```bash
UPLOAD=$(curl -s -X PUT https://scribiz.com/api/v1/uploads \
  -H "Authorization: Bearer $SCRIBIZ_API_KEY" \
  -H "Content-Type: audio/mpeg" \
  -H "X-File-Name: talk.mp3" \
  --data-binary @talk.mp3 | jq -r .upload_id)

curl https://scribiz.com/api/v1/context \
  -H "Authorization: Bearer $SCRIBIZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"upload_id\": \"$UPLOAD\", \"mode\": \"audio\"}"
```

```json title="201 Created, from PUT /v1/uploads"
{
  "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"
}
```

An upload is kept for one hour, belongs to one job, and is removed when that job succeeds. A file with no audio track, such as a screen recording, works with `"mode": "visual"`. For anything over 100 MB, cut the audio first with `ffmpeg` as above. Or use the [CLI](https://scribiz.com/docs/cli.md), which cuts the audio on your machine and reads it with your own Gemini key or your Scribiz account.

## Next

- Every route: [Endpoints](https://scribiz.com/docs/api/endpoints.md).
- Handling failures and limits: [Errors and limits](https://scribiz.com/docs/api/errors.md).

---

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