# API reference

> Every endpoint of the FrameQuery API v1 with its parameters, a copy-pasteable curl command and the shape it returns. The OpenAPI 3.1 document is the machine-readable version.

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

Base URL `https://api.framequery.com/v1/api`. The machine-readable contract is the OpenAPI 3.1 document at [https://api.framequery.com/v1/api/openapi.json](https://api.framequery.com/v1/api/openapi.json); anchors on this page equal its `operationId`s.

All examples assume `export FRAMEQUERY_API_KEY=fq_live_...` and, where a job is involved, `JOB_ID` holding a job id. Successful responses are wrapped as `{"data": …, "requestId": "…"}` (except `GET /openapi.json`); errors follow the [error envelope](https://www.framequery.com/docs/errors).

| Method | Path | Auth | Operation |
|---|---|---|---|
| `POST` | `/jobs` | key | [createJob](#createJob) |
| `POST` | `/jobs/from-url` | key | [createJobFromUrl](#createJobFromUrl) |
| `POST` | `/jobs/batch` | key | [createBatch](#createBatch) |
| `GET` | `/jobs` | key | [listJobs](#listJobs) |
| `GET` | `/jobs/{jobId}` | key | [getJob](#getJob) |
| `POST` | `/jobs/{jobId}/cancel` | key | [cancelJob](#cancelJob) |
| `DELETE` | `/jobs/{jobId}` | key | [cancelJobDeprecated](#cancelJobDeprecated) |
| `GET` | `/jobs/{jobId}/transcript` | key | [getTranscript](#getTranscript) |
| `GET` | `/jobs/{jobId}/scenes` | key | [getScenes](#getScenes) |
| `GET` | `/jobs/{jobId}/audioTracks` | key | [listAudioTracks](#listAudioTracks) |
| `GET` | `/jobs/{jobId}/audioTracks/{trackIndex}` | key | [getAudioTrack](#getAudioTrack) |
| `GET` | `/quota` | key | [getQuota](#getQuota) |
| `GET` | `/webhooks/deliveries` | key | [listWebhookDeliveries](#listWebhookDeliveries) |
| `POST` | `/webhooks/test` | key | [sendTestWebhook](#sendTestWebhook) |
| `GET` | `/openapi.json` | none | [getOpenApi](#getOpenApi) |
| `GET` | `/pricing` | none | [getPricing](#getPricing) |
| `GET` | `/pricing/estimate` | none | [estimatePrice](#estimatePrice) |
| `GET` | `/formats` | none | [listFormats](#listFormats) |
| `GET` | `/keys` | session | [listKeys](#listKeys) |
| `GET` | `/keys/portal` | session | [getKeysPortal](#getKeysPortal) |
| `POST` | `/keys/{id}/rotate` | session | [rotateKey](#rotateKey) |
| `GET` | `/webhooks/secret` | session | [getWebhookSecret](#getWebhookSecret) |
| `POST` | `/webhooks/secret/rotate` | session | [rotateWebhookSecret](#rotateWebhookSecret) |

**key**: an `fq_live_` API key in `Authorization: Bearer` or `X-API-Key`. **none**: no key, rate-limited per IP. **session**: the FrameQuery dashboard's signed-in session, used by the [API keys page](https://www.framequery.com/profile/api-keys); an API key cannot call these.

## Jobs

### Create a job with an upload

`POST /jobs`. Creates a job in state `awaiting_upload` and returns the Job object plus `uploadUrl`, `resumableUploadUrl`, `uploadMethod` (`PUT`), `uploadHeaders`, `expiresInSeconds` (3600) and `maxUploadBytes`; for separate audio also `audioUploadUrl` or `audioTrackUploadUrls`, and `audioUploadHeaders`. Accepts `Idempotency-Key`. Details: [Upload a local file](https://www.framequery.com/docs/jobs#upload).

Body: `fileName` (required), `fileSizeBytes`, `processingMode`, `callbackUrl`, `idempotencyKey`, `metadata`, `audioFileName`, `audioSyncMode`, `audioOffsetMs`, `audioTracks[]`.

```bash
curl -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": "interview.mp4", "fileSizeBytes": 734003200, "processingMode": "all"}'
```

Errors: `400`, `401`, `402`, `403`, `409`, `413`, `415`, `422`, `429`, `500`, `503`.

### Create a job from a URL

`POST /jobs/from-url`. Creates a job that fetches its media from a public http(s) URL; returns the Job object in state `queued`. Accepts `Idempotency-Key`. Details: [Create a job from a URL](https://www.framequery.com/docs/jobs#from-url).

Body: `url` (required), `fileName`, `downloadToken`, `provider` (`gdrive` \| `dropbox`), `processingMode`, `callbackUrl`, `idempotencyKey`, `metadata`, `audioUrl`, `audioDownloadToken`, `audioSyncMode`, `audioOffsetMs`, `audioTracks[]`.

```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"}}'
```

Errors: `400`, `401`, `402`, `403`, `409`, `415`, `422`, `429`, `500`, `503`.

### Create a batch

`POST /jobs/batch`. Creates jobs for up to 100 URLs. Returns `{batchId, mode, jobs[]}`, each item a Job object plus its clip `index`. Accepts `Idempotency-Key`. Details: [Batch jobs](https://www.framequery.com/docs/jobs#batch).

Body: `mode` (`independent` \| `continuous`, required), `clips[]` (1 to 100, each `{sourceUrl, fileName?, downloadToken?, provider?, metadata?}`, required), `processingMode`, `callbackUrl`, `idempotencyKey`, `metadata`.

```bash
curl -X POST https://api.framequery.com/v1/api/jobs/batch \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"mode": "independent", "clips": [{"sourceUrl": "https://example.com/a.mp4"}, {"sourceUrl": "https://example.com/b.mp4"}]}'
```

Errors: `400`, `401`, `402`, `403`, `409`, `415`, `422`, `429`, `500`, `503`.

### List jobs

`GET /jobs`. Your API jobs, newest first. Returns `{data: [Job summary], hasMore, nextCursor}` and a `Link: <…>; rel="next"` header while more pages exist. Continue while `hasMore` is `true`; pages can be short.

Query: `limit` (1 to 100, default 20), `cursor` (the previous `nextCursor`), `state` (a job state; `uploading` is accepted as an alias of `awaiting_upload`), `status` (deprecated legacy filter, not combinable with `state`).

```bash
curl "https://api.framequery.com/v1/api/jobs?state=completed&limit=20" \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY"
```

Errors: `400`, `401`, `429`, `500`.

### Get a job

`GET /jobs/{jobId}`. The [Job object](https://www.framequery.com/docs/job-lifecycle#the-job-object).

Query: `include`, a comma list of `scenes` and `transcript`, or `none`. Default: both, each present only once its stage is `completed`. Use `include=none` when polling.

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

Errors: `400`, `401`, `404`, `429`, `500`.

### Cancel a job

`POST /jobs/{jobId}/cancel`. No body. Allowed while `awaiting_upload`, `queued` or `processing`; returns the Job object (without `processedData`) and emits `job.cancelled`. Completed stages stay charged and readable. Details: [Cancel a job](https://www.framequery.com/docs/jobs#cancel).

```bash
curl -X POST "https://api.framequery.com/v1/api/jobs/$JOB_ID/cancel" \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY"
```

Errors: `401`, `404`, `409` (`JOB_NOT_CANCELLABLE`), `429`, `500`.

### Cancel a job (deprecated)

`DELETE /jobs/{jobId}`. Deprecated alias of `POST /jobs/{jobId}/cancel` with identical behaviour. Responses carry `Deprecation: true` and `Link: </v1/api/jobs/{jobId}/cancel>; rel="successor-version"`. The `DELETE` verb is reserved for a future "delete job and results" operation.

```bash
curl -X DELETE "https://api.framequery.com/v1/api/jobs/$JOB_ID" \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY"
```

## Results

### Get the transcript

`GET /jobs/{jobId}/transcript`. JSON `{jobId, language, speakers, text, segments[], tracks?, trackIndex?, trackName?}`, or a caption file. Details: [Results](https://www.framequery.com/docs/results#transcript).

Query: `format` (`json` default, `srt`, `vtt`, `txt`), `track` (audio track index; required for `srt`/`vtt`/`txt` on multi-track jobs).

```bash
curl "https://api.framequery.com/v1/api/jobs/$JOB_ID/transcript?format=vtt" \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY" -o transcript.vtt
```

Errors: `400`, `401`, `404` (`NOT_FOUND`, `RESULTS_UNAVAILABLE`), `409` (`RESULTS_NOT_READY`), `500`.

### Get scenes

`GET /jobs/{jobId}/scenes`. `{jobId, scenes[]}`, each scene `{index, startTime, endTime, description, objects, colorPalette, shotType, shotAngle}`. Empty when the video has no scene cuts. Details: [Results](https://www.framequery.com/docs/results#scenes).

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

Errors: `401`, `404` (`NOT_FOUND`, `RESULTS_UNAVAILABLE`), `409` (`RESULTS_NOT_READY`), `500`.

### List audio tracks

`GET /jobs/{jobId}/audioTracks`. `{jobId, tracks[]}` for a multi-track job, ordered by index; each `{trackIndex, trackName, status, transcript, speakers, language, errorCode?}` with the stored transcript entries.

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

Errors: `401`, `404`, `500`.

### Get one audio track

`GET /jobs/{jobId}/audioTracks/{trackIndex}`. One track, same shape as a `tracks[]` item above.

```bash
curl "https://api.framequery.com/v1/api/jobs/$JOB_ID/audioTracks/0" \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY"
```

Errors: `400`, `401`, `404`, `500`.

## Account

### Credits and usage

`GET /quota`. `data.api` = `{available, balanceCents, heldCents, availableCents, payAsYouGo, monthUsageSeconds, tier, volumeThresholdSeconds}`. When `available` is `false` the balance could not be read right now and the other fields are omitted. The remaining top-level fields describe desktop-app plans and can be ignored. Details: [Credits and billing](https://www.framequery.com/docs/pricing#credits-and-billing).

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

Errors: `401`, `429`.

## Webhooks

### Delivery log

`GET /webhooks/deliveries`. The last 100 deliveries, newest first: `{id, jobId, eventType, urlHost, status, attempts, lastStatus, lastError, disabled, deliveredAt, createdAt}`. Query: `jobId`. Details: [Delivery log](https://www.framequery.com/docs/webhooks#deliveries).

```bash
curl "https://api.framequery.com/v1/api/webhooks/deliveries?jobId=$JOB_ID" \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY"
```

Errors: `401`, `429`.

### Send a test event

`POST /webhooks/test`. Sends a signed `webhook.test` event to `url` (HTTPS, port 443, public address; 5 per minute). Returns `{delivered, statusCode, latencyMs, error?}`.

```bash
curl -X POST https://api.framequery.com/v1/api/webhooks/test \
  -H "Authorization: Bearer $FRAMEQUERY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://your-app.example.com/webhooks/framequery"}'
```

Errors: `400`, `401`, `429`.

## Discovery (no key)

All four are rate-limited to 120 requests per minute per client IP and cacheable for five minutes (`Cache-Control: public, max-age=300`). A request that carries a key is served the same way.

### OpenAPI document

`GET /openapi.json`. The OpenAPI 3.1 document as JSON, not wrapped in `data`.

```bash
curl https://api.framequery.com/v1/api/openapi.json -o framequery-openapi.json
```

### Price table

`GET /pricing`. Per-stage rates in US cents per footage-hour for the `standard` and `raw` tiers, plus `volumeThresholdSeconds`, `minBillableSeconds`, `currency`, `unit` and `rounding`. Details: [Pricing](https://www.framequery.com/docs/pricing).

```bash
curl https://api.framequery.com/v1/api/pricing
```

### Price estimate

`GET /pricing/estimate`. The exact charge for one job if every requested stage completes: `{cents, usd, currency, billableSeconds, durationSeconds, tier, mode, audioTracks, priorUsageSeconds}`.

Query: `durationSeconds` (required, 0 to 2 678 400), `tier` (`standard` \| `raw`), `mode` (`all` \| `transcript` \| `vision`), `audioTracks` (1 to 16), `priorUsageSeconds` (billable seconds already used this UTC month).

```bash
curl "https://api.framequery.com/v1/api/pricing/estimate?durationSeconds=3600&tier=raw&mode=all"
```

Errors: `400` (with `param`), `429`.

### Formats and limits

`GET /formats`. `{standard, audioOnly, raw: {r3d, braw, arri}, rawExtensions, blocked, unknownExtensions, limits: {standardMaxBytes, rawMaxBytes, maxDurationSec, uploadUrlExpirySeconds}}`. Details: [Formats and limits](https://www.framequery.com/docs/formats).

```bash
curl https://api.framequery.com/v1/api/formats
```

## Dashboard (session)

These power the [API keys page](https://www.framequery.com/profile/api-keys). They take the dashboard's signed-in session token, not an API key, and are listed here because they are part of the contract. `$FRAMEQUERY_SESSION_TOKEN` below stands for that token.

### List keys

`GET /keys`. Your Polar license keys, never the full secret: `[{id, displayKey, status, expiresAt, benefitId, createdAt}]`.

```bash
curl https://api.framequery.com/v1/api/keys -H "Authorization: Bearer $FRAMEQUERY_SESSION_TOKEN"
```

### Customer portal

`GET /keys/portal`. `{url}`: a short-lived Polar customer-portal link to view keys, receipts and credits. `404` if you have never bought anything.

```bash
curl https://api.framequery.com/v1/api/keys/portal -H "Authorization: Bearer $FRAMEQUERY_SESSION_TOKEN"
```

### Rotate a key

`POST /keys/{id}/rotate`. Rotates one key. The response is the key object plus `key`, the new full secret, shown once.

```bash
curl -X POST "https://api.framequery.com/v1/api/keys/$KEY_ID/rotate" -H "Authorization: Bearer $FRAMEQUERY_SESSION_TOKEN"
```

### Reveal the webhook secret

`GET /webhooks/secret`. `{secret, createdAt, rotatedAt}`; the `whsec_` secret is created on the first call.

```bash
curl https://api.framequery.com/v1/api/webhooks/secret -H "Authorization: Bearer $FRAMEQUERY_SESSION_TOKEN"
```

### Rotate the webhook secret

`POST /webhooks/secret/rotate`. Returns the new secret. The previous one keeps signing, as a second `v1,` signature, for 24 hours.

```bash
curl -X POST https://api.framequery.com/v1/api/webhooks/secret/rotate -H "Authorization: Bearer $FRAMEQUERY_SESSION_TOKEN"
```
