# SDKs

> Official Python and TypeScript SDKs (0.2) with uploads, polling, retries, typed errors and webhook verification. Go, Rust and Ruby SDKs at 0.1.

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

All SDKs are open source in [FrameQuery/framequery-sdks](https://github.com/FrameQuery/framequery-sdks). They follow one design, so method names, field names and error classes match across languages, and they match the API: every field is the JSON name in the language's usual case (`jobId` is `job_id` in Python, `jobId` in TypeScript).

| Language | Version | Install |
|---|---|---|
| Python 3.9+ | 0.2 | `pip install framequery` |
| TypeScript / JavaScript (Node 18+, Deno, Bun, edge) | 0.2 | `npm install framequery` |
| Go | 0.1 | `go get github.com/framequery/framequery-go` |
| Rust | 0.1 | `cargo add framequery` |
| Ruby | 0.1 | `gem install framequery` |

The Go, Rust and Ruby SDKs are at 0.1 and move to the 0.2 surface next; see their READMEs in the [SDK repository](https://github.com/FrameQuery/framequery-sdks). This page documents 0.2.

## What the SDKs do for you

- **Uploads** straight to storage, streamed from disk and never read into memory whole: one `PUT` up to 32 MiB, resumable 16 MiB chunks above that, resumed automatically after network errors.
- **Waiting** on `state` with sensible backoff; `wait()` returns the terminal job and never raises for `failed`.
- **Retries** on `429`, `503` and connection errors, honouring `Retry-After`, with an idempotency key on every create so a retry never creates a second job.
- **Typed errors** carrying `code`, `doc_url` / `docUrl`, `param`, `request_id` / `requestId` and `retry_after` / `retryAfter`.
- **Webhook verification** without extra dependencies.

## Client

```python
from framequery import FrameQuery, AsyncFrameQuery

fq = FrameQuery()                       # api_key defaults to FRAMEQUERY_API_KEY
fq = FrameQuery(api_key="fq_live_...", timeout=60, max_retries=3)

async with AsyncFrameQuery() as afq:    # same methods, as coroutines
    job = await afq.jobs.get(job_id)
```

```typescript
import FrameQuery from "framequery";

const fq = new FrameQuery(); // apiKey defaults to FRAMEQUERY_API_KEY
const fq2 = new FrameQuery({ apiKey: "fq_live_...", timeout: 60_000, maxRetries: 3 });
```

Keyless calls (`pricing.get`, `pricing.estimate`, `formats.list`) and webhook verification work without a key.

## Process in one call

`process()` creates the job (upload or URL), waits for it, and fetches whichever results completed.

```python
result = fq.process("interview.mp4", processing_mode="all", metadata={"assetId": "A003_C008"})
result.job            # Job (state "completed")
result.transcript     # Transcript, or None if that stage was skipped
result.scenes         # list[Scene], or None if that stage was skipped
```

```typescript
const result = await fq.process("./interview.mp4", { processingMode: "all", metadata: { assetId: "A003_C008" } });
result.job;        // Job (state "completed")
result.transcript; // Transcript | null
result.scenes;     // Scene[] | null
```

If the job ends `failed` or `expired`, `process()` raises `JobFailedError` (with `job`, `code` and `stage`); `cancelled` raises `JobCancelledError`. Stages that completed are still readable through `fq.jobs.transcript(...)` and `fq.jobs.scenes(...)`.

## Methods

| Operation | Python | TypeScript |
|---|---|---|
| Create and upload a file | `fq.jobs.create(path_or_file, ...)` | `fq.jobs.create(file, {...})` |
| Create, upload elsewhere | `fq.jobs.create_upload(file_name=...)` | `fq.jobs.createUpload({ fileName })` |
| Create from a URL | `fq.jobs.create_from_url(url, ...)` | `fq.jobs.createFromUrl(url, {...})` |
| Batch | `fq.batches.create(clips, mode=...)` | `fq.batches.create(clips, { mode })` |
| Get | `fq.jobs.get(job_id)` | `fq.jobs.get(jobId)` |
| List (auto-paging) | `for job in fq.jobs.list(state="completed")` | `for await (const job of fq.jobs.list({ state: "completed" }))` |
| Wait | `fq.jobs.wait(job_id, timeout=7200)` (seconds) | `fq.jobs.wait(jobId, { timeout })` (ms) |
| Cancel | `fq.jobs.cancel(job_id)` | `fq.jobs.cancel(jobId)` |
| Transcript (JSON) | `fq.jobs.transcript(job_id)` | `fq.jobs.transcript(jobId)` |
| Transcript (SRT, VTT, TXT) | `fq.jobs.transcript(job_id, format="srt")` | `fq.jobs.transcript(jobId, { format: "srt" })` |
| Scenes | `fq.jobs.scenes(job_id)` | `fq.jobs.scenes(jobId)` |
| Balance | `fq.quota.get()` | `fq.quota.get()` |
| Price table | `fq.pricing.get()` | `fq.pricing.get()` |
| Price estimate | `fq.pricing.estimate(duration_seconds, ...)` | `fq.pricing.estimate(durationSeconds, {...})` |
| Formats | `fq.formats.list()` | `fq.formats.list()` |
| Verify a webhook | `fq.webhooks.verify(payload, headers)` | `await fq.webhooks.verify(payload, headers)` |
| Verify and parse | `fq.webhooks.unwrap(payload, headers)` | `await fq.webhooks.unwrap(payload, headers)` |
| Delivery log | `fq.webhooks.list_deliveries(job_id=...)` | `fq.webhooks.listDeliveries({ jobId })` |
| Test delivery | `fq.webhooks.send_test(url)` | `fq.webhooks.sendTest(url)` |

`jobs.get` sends `include=none` by default; results come from `transcript()` and `scenes()`.

## Errors

| Class | When |
|---|---|
| `InvalidRequestError` | `400`, `413`, `415`, `422`; `param` names the field |
| `AuthenticationError` | `401`, or no key configured (raised before any request) |
| `InsufficientCreditsError` | `402` |
| `PermissionDeniedError` | `403` |
| `NotFoundError` | `404 NOT_FOUND` |
| `ResultsUnavailableError` | `404 RESULTS_UNAVAILABLE`: stop; see `reason` |
| `ConflictError` | `409 JOB_NOT_CANCELLABLE`, `IDEMPOTENCY_IN_PROGRESS` |
| `ResultsNotReadyError` | `409 RESULTS_NOT_READY`: try later |
| `RateLimitError` | `429` |
| `InternalServerError` | `5xx` |
| `APIConnectionError`, `APITimeoutError` | No HTTP response |
| `JobFailedError`, `JobCancelledError`, `WaitTimeoutError` | From `process()` / `wait()` |
| `UploadError` | The storage upload failed |
| `WebhookVerificationError` | Bad signature, headers or timestamp |

All API errors derive from `APIError`, and everything from `FrameQueryError`.

```python
from framequery import InsufficientCreditsError, RateLimitError, ResultsNotReadyError

try:
    transcript = fq.jobs.transcript(job_id)
except ResultsNotReadyError as err:
    print("not yet, retry in", err.retry_after, "s")
except InsufficientCreditsError as err:
    print("top up:", err.doc_url)
```

```typescript
import { ResultsNotReadyError, InsufficientCreditsError } from "framequery";

try {
  const transcript = await fq.jobs.transcript(jobId);
} catch (err) {
  if (err instanceof ResultsNotReadyError) console.log("not yet, retry in", err.retryAfter, "s");
  else if (err instanceof InsufficientCreditsError) console.log("top up:", err.docUrl);
  else throw err;
}
```

## Webhooks

See [Webhooks](https://www.framequery.com/docs/webhooks#verify) for complete handlers. The secret defaults to `FRAMEQUERY_WEBHOOK_SECRET`. In Python, `framequery.webhooks.verify(payload, headers, secret)` and `framequery.webhooks.unwrap(...)` also work without a client. Always pass the raw request body.

## Other languages and plain HTTP

Any HTTP client works: see the [API reference](https://www.framequery.com/docs/api) for every endpoint, or generate a client from the [OpenAPI document](https://api.framequery.com/v1/api/openapi.json).
