POST
/ask
API key
Send a video with your prompt and get the answer back.
multipart/form-data
prompt · string — required. Your instructions; this decides the output.
file · file — one field per upload, up to 8. Video, image or PDF.
timeoutMs · number — how long to allow for processing, up to 600000. Give video a generous value: it is the budget for the whole call, including the automatic retry described below.
progressId · string — optional. Enables progress polling for this request.
Stateless by design. There is no conversation, no session and no memory: this call sees only what you send in it, and nothing carries to the next request. Send any context you need inside prompt.
application/json
The same fields, minus file — for text-only requests and prompt iteration.
Response 200
{"ok":true,
"reply":"00:00.000 Masked Young Man: …",
"callId":"call-9f2c4d1e-…"}
reply is the answer as text. Ask for JSON in your prompt and it parses directly.
callId identifies this exact request — log it.
If the answer had to be recovered (the model started over mid-answer), the same response also carries repaired: true — see Response object.
On timeout (504)
The response also carries partial: whatever had been produced when the limit was reached. It is best-effort text — cut off mid-word by definition, so it is not guaranteed to parse and may still carry a JSON prefix or fence the model added. Salvage whole objects out of it if you want to use it; do not build a recovery path on it.
timeoutMs is the whole call, and it is enforced. The budget covers browser start, the upload and transcode, and the answer — so a video needs tens of seconds of headroom before the model even starts. A value outside 5000–600000 is refused with 400 BAD_TIMEOUT rather than silently adjusted.
A timed-out call is still billed. The request was processed, so it counts against your allowance even though it produced nothing usable — see Throughput & limits.
Example
curl -X POST https://169-58-121-148.sslip.io/ask \
-H "x-api-key: $FRAMEWISE_KEY" \
-F "prompt=Return JSON, one object per scene with start, end,
location, characters and a one-line description." \
-F "timeoutMs=600000" \
-F "file=@episode.mp4"
GET
/progress/:progressId
API key
What a long-running request is currently doing.
Use the same progressId you sent with the request. Before it is registered the status reads pending.
{"ok":true,"status":"running",
"steps":[{"name":"processing","detail":"segment 3 of 12"},
{"name":"transcribing","detail":"5381 characters so far"}]}
status — pending · running · done · failed
POST
/jobs
API key
Start a long title and get its segment plan.
Body
label · string — your own name for the title.
durationMs · number — the title's length; required for a plan.
chunkSeconds · number — segment length, default 300.
Response 201
{"ok":true,
"job":{"id":"job-8a66d77014","planned":12,"done":0,"failed":0},
"plan":[{"index":0,"id":"job-8a66d77014-c000","startMs":0,"endMs":300000,
"start":"00:00.000","end":"05:00.000"}, …]}
Divide the title at startMs/endMs. Those offsets keep timing continuous when segments are combined.
POST
/jobs/:id/chunks
API key
Send one segment, with your prompt.
multipart/form-data
Everything POST /ask accepts, plus:
index · number — required. The segment's position from the plan. An index the plan does not contain is refused with 400 CHUNK_INDEX_OUT_OF_PLAN.
startMs · number — the segment's offset in the title. Send the plan's own value (or omit it): a different number is refused with 400 CHUNK_OFFSET_MISMATCH, because it is the plan that places your lines, not the field.
Response 200
{"ok":true,"mergedReady":false,
"chunk":{"id":"job-8a66d77014-c001","index":1,"startMs":300000,
"status":"done","entries":[{"start":"00:02.000", …}]}}
mergedReady becomes true once every segment has finished.
A segment that cannot be read is stored as status: "failed" with its reason and the job records it as missing. Resend only that index.
Resending an index you already sent replaces it — a resend is safe and never counts twice.
GET
/jobs/:id/merged
API key
The whole title combined, in order, with continuous timing.
{"ok":true,"jobId":"job-8a66d77014","label":"Ep 87",
"complete":true,"chunksDone":12,"chunksPlanned":12,"missing":[],
"monotonic":true,"durationMs":3600000,"coveredMs":3590000,"beyondEnd":[],
"entries":[{"start":"00:00.000","end":"00:01.000","character":"Evelyn",
"text_original":"…","text_target":"…"}, …]}
complete — true only when every segment finished. Counted against the plan, so it can never go true with a hole in the title.
missing — [{index,start,end}] for any that did not, so a partial result is never mistaken for a complete one.
Timing is cumulative and the array is sorted, so it reads from the first line of the title to the last. Each segment is placed by the plan's own offset, not by whatever startMs was sent.
monotonic — the timeline never goes backwards. coveredMs — how far into the title the transcript reaches, against durationMs. beyondEnd — any entry that starts or ends past the title, named by segment; empty is the healthy case.
Each entry also carries _chunkIndex so you can tell which segment produced it. The fields inside are whatever your prompt asked for.
GET
/jobs/:id
API key
Per-segment status for a title. No content.
{"ok":true,"job":{"id":"job-8a66d77014","planned":12,"done":11,"failed":1,
"chunks":[{"id":"job-8a66d77014-c003","index":3,"startMs":900000,
"status":"failed","entries":0,"error":"…"}]}}
GET
/jobs
API key
All of your jobs and their progress.
{"ok":true,"jobs":[{"id":"job-8a66d77014","label":"Ep 87","planned":12,"done":12,"failed":0}]}
DELETE
/jobs/:id
API key
Remove a job and its stored results.
{"ok":true}AuthenticationOne key, sent as a header.
Send either x-api-key: <key> or Authorization: Bearer <key>.
Keep the key server-side. A wrong or missing key returns 401.
Rotate the key if it is ever exposed — requests are authorised against the current key only.
GET
/account/usage
API key
What your allowance is, and what is left of it.
The same key you use for /ask works here — send it and you get back the account's plan, counters and remaining allowance. Use it to budget a batch before you start one, and to check how much room is left mid-run.
{"ok":true,
"account":{"id":"user-…","email":"you@studio.com","plan":"starter",
"usage":{"asks":412,"media":96,"lastAt":"2026-10-09T11:02:00.000Z"},
"quota":{"used":412,"limit":500,"remaining":88,"exceeded":false}}}
quota is null when the account has no cap. remaining is what you can still spend.
The counter moves when a request is processed, so read it after a call to see how much that call cost you.
Throughput & limitsWhat you can send at once, and when to back off.
| Limit | Value | What to do |
|---|---|---|
| Requests processed at once | 1 | Send sequentially. Parallel calls do not go faster. |
| Waiting in the queue | 5 | Up to six requests are in flight for your account; a seventh is refused. |
| Queue full | 429 | Honour Retry-After (30 s) and retry with backoff. |
| A text request | seconds | Roughly 4–8 per minute is the ceiling. |
| A video request | 1–3 min per episode | Length drives it, not file size. Add a timeoutMs to match. |
| Upload | 200 MB · 8 files | Split longer footage with the jobs API. |
| timeoutMs | 5 000–600 000 | Default 180 000. The whole call, enforced; outside the range is a 400. |
| A timed-out request | still billed | It was processed, so it counts against your allowance even though it returned nothing usable. |
A timeout costs you the same as a success. The request reaches the model, so it spends allowance whether or not an answer comes back — which is why tuning timeoutMs downwards to fail fast does not save anything. Size the budget to the work and let it finish.
Throughput comes from sending work in sequence, not in parallel. Twelve parallel calls take as long as twelve sent one after the other — the only difference is that the ones past the queue are refused with 429.
A request that is still being processed holds the next one back, which is why timeoutMs matters: an over-long value keeps everyone waiting, an over-short one cuts a real answer off.
Response objectWhat comes back is defined by your prompt.
| Field | Type | Meaning |
|---|---|---|
| ok | boolean | Whether the request succeeded. |
| reply | string | The answer as text. Your prompt decides whether it is prose or JSON. |
| callId | string | Identifies this request. Log it. |
| repaired | boolean | Present and true when the answer had to be recovered before it was usable — the model started over mid-answer, or wrapped the JSON in prose. |
| lossy | boolean | Present with repaired. true means content was lost recovering it, so the reply may cover less than the whole file — the signal to re-run. A recovery without it lost nothing. |
| attempts | number | Present when more than one attempt was needed. |
| partial | string | Present on a 504 only — best-effort text produced before the timeout; not guaranteed to parse. |
| error | object | Present on failure: {code, message, retryable}. |
An ok:true reply is checked before it reaches you. If the model declines instead of answering, or answers with something that is not the JSON your prompt asked for, the request is tried again inside your timeoutMs — and if that still fails you get a failure, never an unusable reply.
When your prompt asks for JSON, reply parses. That is the contract to code against — json.loads(reply) must not raise. If the model wraps the array in prose (a JSON line, "Here is the array:", a code fence) you are handed the JSON alone, flagged with repaired, rather than the wrapper.
repaired means the answer had to be recovered. Add lossy to know whether anything was lost: with lossy: true the reply is valid but may cover less than the whole file, so log it and re-run the item if you need every line. Without it, the recovery dropped nothing and the reply is complete.
Because the schema is yours, there is no fixed field list beyond these — ask for the names you want and they come back in reply.
Note there is no conversation or message id. Nothing is retained between requests, so there is nothing to continue and nothing to expire.
Prompt recipesCopy, edit or ignore.
Timed transcript
Transcribe this video. Return JSON with one object per line:
{"start":"MM:SS.mmm","end":"MM:SS.mmm","speaker":"…","text":"…"}
Split at natural pauses and speaker changes.
Dubbing script with translation
Transcribe and segment for dubbing. Return a JSON array with start, end, character, gender, type, emotion, text_original, and text_target translated into Khmer and adapted to fit each line's duration.
Scene breakdown
Return JSON, one object per scene, with "start", "end", "location", "characters" (array) and "description" (one line).
Recap
Summarise this episode in 150 words, then a single-sentence hook. Plain text.
Content check
Return JSON listing any segment with violence, strong language or unsafe behaviour, with "start", "end" and "reason". Empty array if none.
ErrorsOne shape for every failure.
{"ok":false,
"callId":"call-…",
"error":{"code":"…","retryable":true,"message":"…"}}
Every failure carries the same four fields — code, retryable, message and callId — whatever caused it: a bad key, an unknown route, a wrong method, a full queue or a failure while processing. retryable is true when retrying the identical request can work (every 429 and 5xx) and false when the request itself has to change (every other 4xx). Branch on it and you never have to map status codes by hand; message says in words what went wrong; quote the callId if you ever need support to trace a call.
| Status | Meaning | Action |
|---|---|---|
| 400 | Unusable request. | Fix it; do not retry unchanged. |
| 401 | Key missing or wrong. | Check the header. |
| 404 | Job or resource not found. | Check the id. |
| 429 | Rate limited or queued. | Back off and retry; Retry-After is set. |
| 499 | You disconnected before the answer. | Nothing to do — the request was abandoned and its queue place released. |
| 500 | Unexpected error. | Retry — it is transient. Then contact support with the callId. |
| 502 | The answer could not be used — MODEL_REFUSED, REPLY_CORRUPTED or EMPTY_REPLY. Already retried once for you. | Send the identical request again; it normally succeeds. The message says what came back instead. |
| 504 | Did not finish inside timeoutMs. | Raise the timeout and retry; partial holds what was produced. |
Match on error.code in your retry logic — it is stable, while messages may be reworded. error.retryable is the one field you can branch on without knowing any code.