Video
Sable Video renders text-to-video and image-to-video jobs through the same key, the same balance, the same budgets, and the same signed receipts as every other billable call.
It is asynchronous, and that is the one thing to internalise before writing any code against it. A render takes roughly 30 seconds to 5 minutes. Unlike image generation, which hands you the picture on the response, video returns a job id immediately and you poll for the result.
Availability
Video is off by default and config-gated. It is live only on a deployment
where an operator has set SABLE_VIDEO_PROVIDERS and configured that
provider's credentials. No video provider is enabled on the production
deployment today. Where it is not enabled, the model list comes back empty and
a generation call returns a configuration error rather than a video.
GET /v1/videos/models is public and is the authoritative check for what, if
anything, is enabled here. An empty data array means video is not available on
this deployment.
# Public: what video models are enabled here (empty when off)
curl https://api.buildsable.com/v1/videos/models{
"object": "list",
"data": [
{
"id": "sable-video",
"kind": "video",
"mode": "text-to-video",
"price_usd_per_second": 0.4,
"max_duration_secs": 10,
"default_duration_secs": 5,
"default_resolution": "1080p",
"durations": [3, 5, 8, 10],
"resolutions": ["720p", "1080p"],
"aspect_ratios": ["16:9", "9:16", "1:1"]
}
]
}
The job lifecycle
| Step | Call | What happens |
|---|---|---|
| 1 | POST /v1/videos/generations | The render is handed to the provider synchronously, so a bad request fails immediately. Returns 202 with a job id. |
| 2 | GET /v1/videos/generations/:id | Poll. status and progress advance. |
| 3 | GET /v1/videos/generations/:id/content | Once status is succeeded, download the bytes. |
| — | POST /v1/videos/generations/:id/cancel | Best-effort stop. |
| — | DELETE /v1/videos/generations/:id | Destroy the stored video now rather than at its TTL. |
Status vocabulary
Fixed and exhaustive — you may switch on it:
| Status | Meaning | Billed? |
|---|---|---|
queued | Accepted; the provider has not started. | No |
running | Rendering. progress is whole percent, 0–100. | No |
succeeded | Done. asset_available is true and /content serves the bytes. | Yes |
failed | The render failed, or the job passed its deadline. error_class says which. | No |
canceled | You cancelled it before the provider produced anything. | No |
expired | It succeeded, and the stored video has since passed its TTL and been destroyed. | Yes (at the time) |
The spelling is succeeded — not ok, not complete.
Generating
curl -X POST https://api.buildsable.com/v1/videos/generations \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-d '{
"model": "sable-video",
"prompt": "a slow pan over a black lake at dusk",
"duration_secs": 5,
"aspect_ratio": "16:9",
"resolution": "1080p"
}'Request fields
| Field | Notes |
|---|---|
model | Required. A sable id from GET /v1/videos/models. |
prompt | Required. Up to 4,000 characters. |
duration_secs | Seconds of output. Rounded up to whole seconds and clamped to the model's maximum. Omitted ⇒ the model default. |
aspect_ratio, resolution, seed | Optional; passed through to the model. |
image | Base64 or a data: URL. Required on an image-to-video model, and refused on a text-to-video one — silently ignoring it would bill you for a video that had nothing to do with your image. |
n | Must be 1. Price is per second, so two jobs cost exactly what n: 2 would. |
sable_run_id | Chains this job's receipt into an agent run. |
Pricing: per second, not per job
Video is priced per second of produced output, because duration is yours to
choose. price_usd_per_second on the model listing is the rate.
Two consequences worth knowing before your first bill:
- The duration is rounded up. Asking for 4.2 seconds bills 5. A model's ceiling is its ceiling: asking for 60 on a 10-second model renders and bills 10.
- Submitting reserves the model's maximum. A credit hold for
max_duration_secs × rateis placed when the job is accepted, and the difference is released when the render finishes. So a 5-second job on a 10-second model briefly reserves twice what it costs. This is what stops a hundred concurrent jobs passing a check that only one of them can pay for; it is a reservation, never a charge, and it never appears in the ledger.
A job that fails, times out, or is cancelled before the provider produced anything bills nothing and releases its reservation.
Cancelling
POST /v1/videos/generations/:id/cancel polls the provider once before deciding,
because the honest answer depends on what actually happened:
- The render is still in progress → the provider is asked to stop, the job
becomes
canceled, and the response says"billed": false. - The render was already finished → the work was performed, so the video is
delivered and billed, and the response says
"billed": truewith"canceled": false.
Read billed; do not assume.
Storage, the TTL, and §3
Sable's privacy contract says prompts, completions and submitted code are never persisted. Video needs one disclosed exception, and here it is plainly:
- The prompt is never stored. It is handed to the provider in the frame that
accepts your request, and only a sha256 prefix —
content_fingerprinton the receipt — survives. Same for an image-to-video init image. - The produced video IS stored. It has to be: you are not on the line when a render finishes minutes later, so there is no "return it once" to do. It is AES-GCM-sealed with the master key (ciphertext at rest — the same posture as hosted agents, Relay and batch files), opened in frame only to serve your own download, and never logged.
- It has a hard TTL.
expires_aton the job object is when the sweeper destroys the ciphertext — 24 hours by default. After that the job row still reads honestly (expired, with its cost, its hashes and its signed receipt intact) and/contentreturns 404. Download what you want to keep. DELETEdestroys it immediately rather than waiting for the TTL. The content-free row and its receipt remain, because the receipt is a durable record of what you were billed for.
Why Sable downloads the video instead of forwarding a provider link
Some backends return a URL rather than bytes. Sable fetches it server-side into the sealed store and never hands you the provider's link. Passing it through would leak your request to a third party outside Sable's contract, and it would expire on that provider's schedule instead of the TTL published here.
The receipt
Every settled job mints a signed, metadata-only receipt, stored like every
other and verifiable at the public
POST /v1/receipts/verify.
{
"v": 1,
"kind": "video",
"request_id": "vid_…",
"model": "sable-video",
"engine": "…",
"provider": "video:…",
"unit": "video_seconds",
"quantity": 5,
"duration_secs": 5,
"resolution": "1080p",
"content_fingerprint": "…",
"output_sha256": "…",
"cost_micro_usd": 2000000,
"latency_ms": 41230,
"created_at": "…"
}
output_sha256 is the hash of the exact bytes produced, so a receipt and the
file can be checked against each other years later.
Each video also carries a signed provenance manifest, the same content-free
shape images use, verifiable at
POST /v1/videos/verify (an alias for the image verifier — one verifier, one
answer). It reports "embedded": false for video: MP4 and WebM are recognised
but the manifest is deliberately not written into the container, because a
half-correct box produces files some players reject. The manifest travels beside
the file instead, and Sable never claims an embedding it did not do.
From an agent
The routes are key-authed, so a hosted agent's injected key
reaches them with no extra plumbing. Over MCP the tools are
sable_generate_video (returns the job id immediately — an MCP call must
never block for five minutes) and sable_video_status (poll it).
Limits and failure classes
| In-flight jobs per account | 4 by default; over it returns 429 with Retry-After. One caller cannot monopolize the poller. |
| Per-job deadline | 15 minutes by default. Past it the job is failed with error_class: "timed_out" and nothing is billed. |
| Asset ceiling | 64 MiB by default, enforced while downloading. |
error_class is always a fixed identifier — provider_failed, no_output,
timed_out, download_failed, job_not_found, provider_unconfigured — never
provider prose, which can echo your prompt back at you.