POST /v1/jobs, it runs on Voyager’s side, and you read the result with GET /v1/jobs/{id} or receive it by webhook.
Create a job
input.cast takes up to 4 cst_… IDs from Cast on image and video operations; other models refuse it with cast_not_supported. The fields input takes are on Models.
The response is 201 Created with the job, usually queued. Unknown top-level fields are refused with 400 invalid_request.
Wait on create
AddPrefer: wait=N (N up to 60) to hold the create request for up to N seconds. If the job finishes in time, the response is 200 OK with the finished job; otherwise it’s 201 Created with the queued or running job and you continue with GET. The response echoes Preference-Applied: wait=N. Use it for fast models; for video and 3D, create without waiting.
The job object
Each entry in
outputs. A signed url is valid until url_expires_at (1 hour); expires_at is when the file itself is deleted. 3D outputs add formats (one downloadable file per format) and a still preview_url.
error, when the job failed or was canceled by Voyager (the codes are on Errors):
Statuses
succeeded, failed and canceled are terminal: a job in one of them never changes status again. Its credits can still change until credits.state is settled or unresolved.
Wait for a result
Long-poll
GET /v1/jobs/{id}?wait=N (N up to 25) holds the request until the job reaches a terminal status or N seconds pass, then returns the job either way. Loop while the status is queued or running:
Plain polling
Withoutwait, GET /v1/jobs/{id} returns at once. While the job is active the response has a Retry-After header (seconds) and poll_after_ms in the body: wait that long before the next request. Polling faster doesn’t make the job finish sooner and counts against your rate limit.
Webhooks
Passwebhook_url on create and Voyager calls you when the job finishes. See Webhooks. GET /v1/jobs/{id} stays the source of truth.
List jobs
GET /v1/jobs lists your jobs in this workspace, newest first.
Credits
Every job is paid from your workspace’s Voyager credits. Thecredits object keeps the job’s result apart from its bill:
Any amount can be
null, which means not known yet, never zero. A job finishing (and its webhook firing) doesn’t mean its charge is final: read credits.state.
To see a cost before you run, call POST /v1/estimate with the same body you’d send to POST /v1/jobs:
An estimate holds no credits and isn’t a cap: the charge is the real cost. When
can_run is false, reason is the error code creating the same job would get; for plan and credit refusals, upgrade_url opens the dashboard. An estimate also needs a paid plan. It never imports https media inputs, except a source recording for a speech, voice or audio model whose price depends on its length: that one is imported into your workspace to measure it and kept as a reference file for 7 days. Send a file_… ID from POST /v1/files to avoid the import.
How each type is charged
A
failed job releases its hold and charges nothing, except under the 3D rule above or an unresolved outcome. A job canceled while still queued charges nothing.
Unresolved outcomes
Rarely, Voyager can’t confirm how a generation went, for example when a model stops answering mid-job. You then getoutcome_unknown (retryable): Voyager couldn’t confirm the generation yet. Ask for its status again with GET /v1/jobs/{id}, with backoff. credits.state can be unresolved: the API won’t state that charge, or the outcome couldn’t be proven; your usage on the dashboard shows what was charged.
Cancel a job
POST /v1/jobs/{id}/cancel returns the job as it stands after the request.
Outputs and retention
- Outputs are files in your workspace, kept for 30 days after the job finishes. Then they’re deleted.
outputs[].urlis a signed link valid for 1 hour that needs no Authorization header. EachGET /v1/jobs/{id}orGET /v1/files/{id}returns a fresh link. Download results you want to keep.- An output’s
file_…ID can be used as an input to another job, for example to edit an image or animate it into a video. - After its outputs are deleted, a job’s results are gone: you get
expired(410), andoutputsis empty. DELETE /v1/files/{id}deletes a file now (204 No Content). A file a running job still uses, or one saved in a Cast card, returns409asset_in_use.
Uploads
Reference images, audio and video go in through/v1/files. Use the returned file_… ID in a job’s input. A media input can also be a public https URL: when you create the job, Voyager imports it into your workspace, like POST /v1/files with a url. The import happens after the plan and credit checks, so a refused request leaves no file behind. An unusable URL is invalid_url; one that can’t be downloaded is fetch_failed.
1
Create the file
2
Upload the bytes
Send the file to
upload_url with method and exactly the headers returned, before upload_expires_at. size_bytes must be the exact size you’ll send. Don’t send your API key to this URL.3
Complete the upload
ready, or failed with an error (wrong type, a size other than declared, or over your plan’s limit). Only ready files can be used in jobs. Completing is safe to repeat; calling it before the bytes are uploaded returns 409 upload_incomplete.{"url": "https://…"} to POST /v1/files. Voyager downloads it and the file comes back ready at once.
The POST /v1/files body:
One of:
FileUpload
FileImport
The file object:
GET /v1/files/{id} returns the file with a fresh signed url to GET /v1/files/{id}/content (valid 1 hour; returns the bytes, answers HEAD, and a single Range request gets 206). expires_at and retention say when it will be deleted.
Idempotency
EveryPOST accepts an Idempotency-Key header. On the calls that create something (jobs, files and their complete, Cast cards and references) Voyager stores the result and replays it as below. Estimates and cancel accept the key too, but don’t replay: repeating them never starts work or spends credits. (An estimate for a speech, voice or audio model priced by a source recording’s length imports an https recording each time it’s called; send a file_… ID from POST /v1/files instead to avoid that.) Send a new UUID per logical request and reuse it on every retry of that request (1–200 printable ASCII characters; anything else is 400 invalid_idempotency_key). It matters most on POST /v1/jobs, where a lost response could otherwise buy twice.
- Same key, same body: you get the original result back (for a job,
200 OKwith anIdempotent-Replayed: trueheader), however long ago it was made. Voyager never buys twice for one key. - Same key, different body:
409idempotency_conflict. Use a new key for a new request. - Same key while the first request is still being processed:
409idempotency_in_flight. Retry shortly with the same key and body. - No key: every request is new. A retry after a lost response can create, and charge for, a second job.
model, operation, input, webhook_url and metadata (key order and whitespace don’t matter). Keys belong to you and the workspace, not to one API key, so a retry from a rotated key still finds the original.
A request refused before it was accepted (400, 401, 402, 403, 404, 422 or 429, including a full queue) doesn’t use up its key: fix the problem and retry with the same key. Once a request is accepted, its key stays bound to its result.