POST /v1/videos generates a video. The endpoint needs an API key sent as
Authorization: Bearer. See Authentication. The body is JSON.
A video takes time to generate, so the endpoint never waits for the result. It always returns 202 with a job. You collect the result by polling the job or by receiving a webhook event. This also applies when you send a request again with the same Idempotency-Key, even if the job has already finished.
Create a video
Section titled “Create a video”Send the model and a prompt. The example uses sakuranbo/cherry-1.0-pro.
curl -X POST "https://api.sakuranbo.ai/v1/videos" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $SAKURANBO_API_KEY" \ -d '{ "model": "sakuranbo/cherry-1.0-pro", "prompt": "Aerial shot gliding over a misty mountain lake at sunrise, gentle wind and birdsong", "duration": 5, "resolution": "1080p", "aspect_ratio": "16:9"}'const response = await fetch("https://api.sakuranbo.ai/v1/videos", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.SAKURANBO_API_KEY}`, }, body: JSON.stringify({ "model": "sakuranbo/cherry-1.0-pro", "prompt": "Aerial shot gliding over a misty mountain lake at sunrise, gentle wind and birdsong", "duration": 5, "resolution": "1080p", "aspect_ratio": "16:9" }),});const data = await response.json();console.log(data);import os
import requests
response = requests.post( "https://api.sakuranbo.ai/v1/videos", headers={ "Authorization": f"Bearer {os.environ['SAKURANBO_API_KEY']}", }, json={ "model": "sakuranbo/cherry-1.0-pro", "prompt": "Aerial shot gliding over a misty mountain lake at sunrise, gentle wind and birdsong", "duration": 5, "resolution": "1080p", "aspect_ratio": "16:9", },)print(response.json())These are the input parameters of the example model:
| Name | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
prompt | string | Yes | Up to 20,000 characters | Description of the scene, motion, and audio. | |
first_frame | url | No | Image to use as the first frame (image-to-video). | ||
last_frame | url | No | Image to use as the last frame. Requires first_frame. | ||
reference_images | url[] | No | Up to 10 items | Up to 10 reference images. Cannot be combined with keyframes. | |
duration | integer | No | 5 | 2 to 30 | Video length in seconds. Billed per second. |
resolution | enum | No | 1080p | One of: 1080p, 2k, 4k | Output resolution. Determines the per-second price. |
aspect_ratio | enum | No | adaptive | One of: adaptive, 16:9, 9:16, 1:1, 4:3, 3:4 | Aspect ratio of the output video. |
audio | boolean | No | true | Generate an audio track. The price is the same with or without audio. |
The request body has these fields:
| Name | Type | Required | Default | Constraints |
|---|---|---|---|---|
model | string | Yes | 1 to 128 characters | |
prompt | string | Yes | ||
first_frame | string | No | ||
last_frame | string | No | ||
reference_images | string[] | No | ||
duration | number | No | ||
resolution | string | No | ||
aspect_ratio | string | No | ||
audio | boolean | No |
A parameter that is not in the table, or a value outside its range, returns 400 with invalid_request. error.param names the parameter, and error.details.allowed, error.details.minimum, or error.details.maximum give the valid values.
Choose the input
Section titled “Choose the input”The fields you send select how the video is made:
- Send only
promptto generate a video from text. - Send
first_frameto start the video from an image. You can also sendlast_frameto end it on an image.last_frameneedsfirst_frame. - Send
reference_imagesto generate a video that uses the images as references. The order matters, because the prompt can refer to the first image or the second image. You cannot combinereference_imageswithfirst_frameorlast_frame.
An image is a public https URL or a data:image/...;base64, URI of a PNG, JPEG, WebP, or GIF image. A data URI has a smaller size limit than a URL, so send a large image as a public URL. The server fetches each URL to check it before it creates the job. It does not follow redirects, and it rejects URLs that have credentials, IP addresses, or internal host names. An image that cannot be used returns 400 with invalid_request. error.param names the field. For a URL, error.details.reason says what is wrong. For a data URI that is too large, error.details.max_bytes is the limit.
The response
Section titled “The response”The 202 response is the job with two extra fields, polling_url and eta_seconds. Its object is video.
| Name | Type |
|---|---|
id | string |
object | string |
model | string |
status | string |
progress | integer | null |
created_at | string |
completed_at | string | null |
expires_at | string | null |
error | object | null |
outputs | object[] |
usage | object | null |
cost | object | null |
polling_url | string |
eta_seconds | integer | null |
progress and eta_seconds are estimates based on the elapsed time. Do not use them as a deadline or as a basis for billing. The Location header and polling_url both point to the video.
Wait for the result
Section titled “Wait for the result”Poll GET /v1/videos/{id} every few seconds until status is completed, failed, cancelled, or expired. The polling_url is the URL of this request. A failed job still returns 200, and the reason is in error. See Jobs for the statuses.
curl "https://api.sakuranbo.ai/v1/videos/job_0123456789abcdef0123456789abcdef" \ -H "Authorization: Bearer $SAKURANBO_API_KEY"const response = await fetch("https://api.sakuranbo.ai/v1/videos/job_0123456789abcdef0123456789abcdef", { headers: { Authorization: `Bearer ${process.env.SAKURANBO_API_KEY}`, },});const data = await response.json();console.log(data);import os
import requests
response = requests.get( "https://api.sakuranbo.ai/v1/videos/job_0123456789abcdef0123456789abcdef", headers={ "Authorization": f"Bearer {os.environ['SAKURANBO_API_KEY']}", },)print(response.json())To avoid polling, register a webhook endpoint and receive job.succeeded, job.failed, and job.cancelled events. See Webhooks.
A job that does not finish within its time limit becomes expired. It is not billed.
A busy model provider does not appear as an HTTP status of POST /v1/videos. It appears later in the error field of the job, with the code rate_limited or concurrency_limited. Create the video again after a short wait.
The model provider can also reject the prompt or a reference image because of its content. This does not appear as an HTTP status either. The job fails with the code content_policy_violation, and nothing is charged.
Download the file
Section titled “Download the file”A completed job has one item in outputs. Its url is a signed URL that needs no API key and works until expires_at, which is 7 days after the job completes. Anyone who has the URL can download the file until then, so treat the URL as a secret. After expires_at the file is deleted, so save the videos you need.
You can also call GET /v1/videos/{id}/content with your API key. It answers with a redirect to the signed URL, so your client must follow redirects. If the job has no file, it returns 404 and error.details.status is the status of the job. This happens when the job is still running, has failed, was cancelled, or expired, and when the file has already been deleted.
List and cancel videos
Section titled “List and cancel videos”GET /v1/videos lists your videos, newest first, with the same limit, cursor,
and status parameters as the job list. POST /v1/videos/{id}/cancel cancels a video that is
queued or running. Both follow the rules of the job endpoints. See Jobs.
Retry safely
Section titled “Retry safely”Send an Idempotency-Key header with a value you choose. If a request with the same key and the same content arrives again, you get the same job and you are charged once. The same key with a different request returns 409 with idempotency_conflict. The key is remembered for a limited time, which is stated in the API reference.
Billing
Section titled “Billing”When a request is accepted, Sakuranbo AI reserves the estimated price from your balance. The estimate comes from the duration and the resolution. When the job completes, you are charged for the requested duration at the per-second price of the resolution, which is the amount that was reserved. A job that is cancelled or expires is not billed. A failed job is normally not billed either, and its reservation returns to your balance.
In rare cases, Sakuranbo AI cannot confirm whether the request reached the model provider. The job then fails with upstream_error, and its reserved amount stays reserved until Sakuranbo AI confirms whether the request is billed. If it stays reserved, contact support with the job ID.
For prices, see the model catalog and the pricing page, or calculate a price before you run the request with POST /v1/estimate. See the Estimate API.
Errors
Section titled “Errors”| Code | HTTP status | Type | Description |
|---|---|---|---|
invalid_request | 400 | invalid_request_error | The request is malformed or a value is out of range. details.allowed lists the valid values when they are known. |
unauthorized | 401 | authentication_error | The API key is missing, malformed, unknown, revoked, or expired. The response does not say which. |
balance_insufficient | 402 | insufficient_balance | The available balance is lower than the amount that must be reserved for this request. error.details.limit_source is balance. |
key_limit_exceeded | 402 | insufficient_balance | This request would take the API key over its spending limit. error.details.limit_source is key, and error.details.resets_at says when the counter starts again. |
account_suspended | 403 | permission_error | The account is suspended, so image, video, and chat requests are refused. Contact support. The balance, deposits, API keys, and jobs stay available. |
model_not_found | 404 | not_found_error | The model ID does not exist in the catalog. The chat endpoints also return it for a model that is not a text model. |
idempotency_conflict | 409 | conflict_error | The Idempotency-Key was already used with a different request. |
payload_too_large | 413 | invalid_request_error | The request body is larger than the allowed size. error.details.max_bytes is the limit. |
upstream_unavailable | 503 | overloaded_error | The upstream provider is temporarily out of capacity. Retry later. |
For a 429 or 503 response, wait the number of seconds in the Retry-After header before you retry. A 402 response has error.details.limit_source, which is balance or key. See Balance and key limits. For the full list of codes, see Errors.
All prices are in USD. Listed prices include our margin.