# Quickstart

> Process your first video in a few minutes. Get a key, create a job from a URL or a local file, wait for it, and read the transcript and scenes.

Source: https://www.framequery.com/docs/quickstart (updated 2026-10-13)

## 1. Get a key

During the public beta, [request access by email](mailto:support@framequery.com?subject=FrameQuery%20API%20beta%20access) first. Once your account is on, buy a credit pack on the [API keys page](https://www.framequery.com/profile/api-keys) (packs start at $10). Polar, our billing provider, issues your key with the purchase; it starts with `fq_live_`. Put it in an environment variable on the machine that will call the API:

```bash
export FRAMEQUERY_API_KEY="fq_live_..."
```

Check it works. `GET /quota` returns your balance:

```bash
curl https://api.framequery.com/v1/api/quota \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY"
```

```json
{
  "data": {
    "api": {
      "available": true,
      "balanceCents": 1000,
      "heldCents": 0,
      "availableCents": 1000,
      "payAsYouGo": false,
      "monthUsageSeconds": 0,
      "tier": "standard",
      "volumeThresholdSeconds": 1800000
    }
  },
  "requestId": "4b0c2f8e9a1d4c67"
}
```

## 2. Install an SDK (optional)

Everything below works with plain HTTP. The SDKs add uploads, polling, retries and typed results:

```bash
# Nothing to install. The examples use curl and jq.
```

```bash
pip install framequery
```

```bash
npm install framequery
```

## 3. Process a video from a URL

Any public http(s) URL works, including presigned S3, R2, B2 or Google Cloud Storage links.

```bash
curl -X POST https://api.framequery.com/v1/api/jobs/from-url \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://example.com/interview.mp4",
    "processingMode": "all",
    "metadata": { "assetId": "A003_C008" }
  }'
```

```python
from framequery import FrameQuery

fq = FrameQuery()  # reads FRAMEQUERY_API_KEY

result = fq.process("https://example.com/interview.mp4", metadata={"assetId": "A003_C008"})
print(result.job.state)                      # "completed"
print(result.transcript.text if result.transcript else "")
for scene in result.scenes or []:
    print(f"{scene.start_time:.1f}s  {scene.description}")
```

```typescript
import FrameQuery from "framequery";

const fq = new FrameQuery(); // reads FRAMEQUERY_API_KEY

const result = await fq.process("https://example.com/interview.mp4", {
  metadata: { assetId: "A003_C008" },
});
console.log(result.job.state); // "completed"
console.log(result.transcript?.text ?? "");
for (const scene of result.scenes ?? []) console.log(`${scene.startTime.toFixed(1)}s  ${scene.description}`);
```

The curl call returns the full Job object straight away, in state `queued`:

```json
{
  "data": {
    "jobId": "0b5e6c1e-6f43-4a0e-9d4c-2f7f3c1b9a10",
    "state": "queued",
    "processingMode": "all",
    "format": { "tier": "standard", "extension": "mp4" },
    "stages": {
      "ingest": { "status": "pending" },
      "vision": { "status": "pending" },
      "transcript": { "status": "pending" }
    },
    "metadata": { "assetId": "A003_C008" },
    "links": {
      "self": "https://api.framequery.com/v1/api/jobs/0b5e6c1e-6f43-4a0e-9d4c-2f7f3c1b9a10",
      "transcript": "https://api.framequery.com/v1/api/jobs/0b5e6c1e-6f43-4a0e-9d4c-2f7f3c1b9a10/transcript",
      "scenes": "https://api.framequery.com/v1/api/jobs/0b5e6c1e-6f43-4a0e-9d4c-2f7f3c1b9a10/scenes",
      "cancel": "https://api.framequery.com/v1/api/jobs/0b5e6c1e-6f43-4a0e-9d4c-2f7f3c1b9a10/cancel"
    }
  },
  "requestId": "9f2a61c0b7e34d18"
}
```

## 4. Or upload a local file

`POST /jobs` creates the job and returns a signed upload URL. Upload straight to it; the bytes never pass through our API servers. Send every header in `uploadHeaders` exactly as given (they are part of the signature) and do not send your API key to the upload URL.

```bash
FILE=interview.mp4
RESPONSE=$(curl -sS -X POST https://api.framequery.com/v1/api/jobs \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"fileName\": \"$FILE\", \"fileSizeBytes\": $(wc -c < "$FILE")}")

JOB_ID=$(echo "$RESPONSE" | jq -r .data.jobId)
UPLOAD_URL=$(echo "$RESPONSE" | jq -r .data.uploadUrl)

# Turn uploadHeaders into -H arguments and PUT the file.
HEADERS=()
while IFS= read -r h; do HEADERS+=(-H "$h"); done \
  < <(echo "$RESPONSE" | jq -r '.data.uploadHeaders | to_entries[] | "\(.key): \(.value)"')
curl -sS -X PUT "$UPLOAD_URL" "${HEADERS[@]}" --upload-file "$FILE"
```

```python
job = fq.jobs.create("interview.mp4")  # creates the job and streams the upload
print(job.job_id, job.state)
```

```typescript
const job = await fq.jobs.create("./interview.mp4"); // creates the job and streams the upload
console.log(job.jobId, job.state);
```

Processing starts as soon as the upload completes. For files over a few hundred megabytes use the resumable upload described in [Jobs](https://www.framequery.com/docs/jobs#upload); the SDKs switch to it automatically above 32 MiB.

## 5. Wait for the job

Poll the job and branch on `state`. `include=none` keeps the response small.

```bash
curl "https://api.framequery.com/v1/api/jobs/$JOB_ID?include=none" \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY"
```

```python
job = fq.jobs.wait(job.job_id, timeout=3600)  # returns the job in a terminal state
print(job.state, job.stages.vision.status, job.stages.transcript.status)
```

```typescript
const done = await fq.jobs.wait(job.jobId, { timeout: 3_600_000 }); // ms in TypeScript
console.log(done.state, done.stages.vision.status, done.stages.transcript.status);
```

The job moves from `queued` (or `awaiting_upload`) to `processing`, then to one terminal state: `completed`, `failed`, `cancelled` or `expired`. Rather than polling, you can pass a `callbackUrl` when you create the job and receive a signed [webhook](https://www.framequery.com/docs/webhooks).

## 6. Read the results

```bash
# Transcript as JSON, then as an SRT file
curl "https://api.framequery.com/v1/api/jobs/$JOB_ID/transcript" \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY"
curl "https://api.framequery.com/v1/api/jobs/$JOB_ID/transcript?format=srt" \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY" -o interview.srt

# Scenes with captions
curl "https://api.framequery.com/v1/api/jobs/$JOB_ID/scenes" \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY"
```

```python
transcript = fq.jobs.transcript(job.job_id)
for seg in transcript.segments:
    print(f"[{seg.start_time:.1f}-{seg.end_time:.1f}] {seg.speaker}: {seg.text}")

srt = fq.jobs.transcript(job.job_id, format="srt")  # str
scenes = fq.jobs.scenes(job.job_id)
```

```typescript
const transcript = await fq.jobs.transcript(job.jobId);
for (const seg of transcript.segments) console.log(`[${seg.startTime}-${seg.endTime}] ${seg.speaker}: ${seg.text}`);

const srt = await fq.jobs.transcript(job.jobId, { format: "srt" }); // string
const scenes = await fq.jobs.scenes(job.jobId);
```

Field-by-field shapes are in [Results](https://www.framequery.com/docs/results).

## Next steps

- [Webhooks](https://www.framequery.com/docs/webhooks): get notified instead of polling, and verify signatures.
- [Pricing and billing](https://www.framequery.com/docs/pricing): what a job costs, and how to estimate it before you send it.
- [Errors](https://www.framequery.com/docs/errors): every error code, and which ones to retry.
- [SDKs](https://www.framequery.com/docs/sdks): Python and TypeScript reference.
