Reframe Video
Change a video's aspect ratio by painting the new areas, optionally guided by reference images, priced from the source video.
Reframe Video (product reframe_video) changes a video's aspect ratio and paints in the areas the new frame adds, optionally guided by up to 2 reference images. One task makes one video, priced from the source video's length and resolution.
Create a task
https://api.nanoapi.ai/v1/tasksUse the shared create endpoint with model: "reframe-video". It usually responds in under 1 second; the source video is read and the cost frozen in the background within a few seconds. Get the result once the task has finished.
Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <API key>, see Authentication. |
Content-Type | Yes | application/json |
Idempotency-Key | No | A unique string for every new task, ideally a UUID (1–255 characters). See Preventing duplicate tasks. |
Body
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | reframe-video. |
video_url | string | Yes | The source video, 4–60 seconds. See Media. |
aspect_ratio | string | Yes | The new aspect ratio: 16:9, 9:16, 4:3, 3:4, 1:1 or 21:9. |
image_urls | string[] | No | Up to 2 reference images that guide what's painted in the new areas. Only for source videos of at most 15 seconds. See Media. |
webhook_url | string | No | Notified when the task succeeds or fails: https, up to 2048 characters, not a private, loopback or reserved address. See Webhooks. |
Turn a clip of at most 15 seconds into 9:16, with a reference image for the new areas:
curl https://api.nanoapi.ai/v1/tasks \
-H "Authorization: Bearer $NANOAPI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f6a8c2e-0b3d-4d9f-9a7e-2c5b1f8d6e30" \
-d '{
"model": "reframe-video",
"video_url": "https://example.com/clip.mp4",
"aspect_ratio": "9:16",
"image_urls": ["https://example.com/background.jpg"]
}'Response: 200 OK. data is the newly created task, with only these fields; each one is described in Create a task: Response. Keep data.id to get the result. The cost is frozen within a few seconds of creation, and the task may fail at that point (for example if the source video is longer than 15 seconds with image_urls): go by what retrieving the task or the webhook tells you.
{
"code": 200,
"message": "success",
"data": {
"object": "task",
"id": "tk_01jbm7f9h2k5m8p1t4w7z0d3e6",
"model": "reframe-video",
"status": "queued",
"error": null,
"created_at": "2026-11-03T08:30:00Z"
}
}Get the result
Measured from creation to the end: 5–10 minutes, about 7 for a 10-second video, slower when the generation service is busy. Once the task has finished (succeeded or failed), get the result either way:
- Webhook (recommended in production): if you passed
webhook_url, the full task is pushed to it once when the task finishes. See Webhooks. - Polling: call
GET /v1/tasks/{task_id}withdata.idat most once every 15 seconds untilstatusissucceededorfailed. Check several tasks at once withidsin List tasks.
The request above, once it has succeeded (other fields left out):
{
"status": "succeeded",
"input": {
"video_url": "https://example.com/clip.mp4",
"aspect_ratio": "9:16",
"image_urls": ["https://example.com/background.jpg"]
},
"num_outputs": 1,
"outputs": [
{
"index": 0,
"type": "video",
"url": "https://cdn.nanoapi.ai/h/tk_01jbm7f9h2k5m8p1t4w7z0d3e6_1_1793694600.mp4",
"preview_url": "https://cdn.nanoapi.ai/h/tk_01jbm7f9h2k5m8p1t4w7z0d3e6_1_1793694600_compressed.mp4"
}
],
"billing": {
"currency": "USD",
"frozen": "…", // worked out from the source video, see Pricing
"charged": "…", // the same, occasionally a little less
"refunded": "0.00",
"billed_seconds": null
}
}| Field | Reframe Video |
|---|---|
outputs | One item. |
outputs[0].type | video. |
outputs[0].url | The reframed video, an mp4, for downloading. Its resolution follows the source video (480p, 720p or 1080p) and can't be chosen; a source with a short side above 1080 pixels is scaled down to 1080 first. |
outputs[0].preview_url | A silent, compressed h264 mp4 at most 600 pixels wide, for display. null if it couldn't be made. |
billing.billed_seconds | Always null: the cost is worked out from the source video, see Pricing. |
input | The parameters you sent; image_urls only if you sent it. |
On failure the frozen amount is returned in full and the reason is in data.error.type, see Errors. Every field is described in The task object.
Media
All media is passed as public http(s) URLs. There's no upload endpoint: host the files yourself. URLs are at most 4096 characters, must download directly and can't point to localhost or private, loopback or reserved addresses.
Source video (video_url) | Reference images (image_urls) | |
|---|---|---|
| Count | 1 | 0–2 (limits.max_reference_images in GET /v1/products) |
| Length | 4–60 seconds (limits.min_source_video_seconds / limits.max_source_video_seconds); at most 15 seconds when you send image_urls (limits.max_source_video_seconds_with_images) | – |
| If there's a problem | Read within a few seconds of creating the task: if it can't be read (including its length or size), the task fails with invalid_media; outside 4–60 seconds, with video_too_short / video_too_long; longer than 15 seconds with image_urls, with invalid_request. Nothing is frozen. | Read only when generation runs (the price is worked out from the source video alone): if one can't be downloaded or read, the task fails with invalid_media and the frozen amount is returned in full. A failure within a few seconds of creating the task was found before freezing: billing.frozen stays "0.00". |
Keep every link reachable until the task finishes, including links that expire.
Pricing
cost = per-task amount + price per second (source resolution) × seconds of the source videoLoading the current prices…
- Seconds: the source video's length, rounded to the nearest second.
- Price per second: by the source video's resolution tier, 480p, 720p or 1080p, set by the generation service when it reads the source video. For example, a 640×360 video is billed as 480p, and a short side above 1080 pixels counts as 1080p.
aspect_ratioand reference images don't change it. - Within a few seconds of creating the task, the source video is read, the cost worked out and frozen. When the task succeeds, the frozen amount is charged, occasionally a little less (some videos are processed again during generation, for example hidden frames left by phone editing, or re-encoding), never more. When it fails, all of it is returned. How freezing and charging work in general: Billing.
billing.billed_secondsis alwaysnull.
Example: if the source video of the request above is a 10-second 720p clip, the task costs the per-task amount plus 10 × the 720p price per second.
To show a price before submitting, read the video's length and resolution on your side and apply the current prices from GET /v1/products: the reframe_video product has one prices row per source resolution, with the per-task amount in base_price. Prices change, so don't hard-code them. It's an estimate, since the tier is set only when the source video is read: billing.frozen on the retrieved task is what counts (it's frozen within a few seconds of creation).
Errors
Errors and failure reasons specific to this model. Every error of the create endpoint: Create a task → Errors; every failure reason: Failure reasons.
Create request errors: the request fails with an HTTP error and no task is created. param names the parameter.
| Status | error.type | When | What to do |
|---|---|---|---|
400 | invalid_request | video_url or aspect_ratio is missing (Field required, param names it). | Send both. |
400 | invalid_request | aspect_ratio isn't one of the values above (Input should be '16:9', '9:16', '4:3', '3:4', '1:1' or '21:9' for reframe-video), image_urls has more than 2 images (param "image_urls"), a link isn't a public http(s) URL of at most 4096 characters, or num_outputs isn't 1. | Fix the parameter param names. |
400 | invalid_request | A parameter this model doesn't take, such as prompt (Extra inputs are not permitted). | Remove it. |
Other errors of the create endpoint (401, 402, 429 and so on): Create a task → Errors.
Task failures: the create request succeeded (200) and the task later turned failed. There's no HTTP status: the reason is the task's error.type, from retrieving the task or the webhook.
error.type | When | What to do |
|---|---|---|
invalid_media | The source video can't be read, including its length or size. Within a few seconds of creating the task, nothing frozen. | Use a public, direct link to the video and create the task again. |
invalid_media | A reference image can't be downloaded (A reference image could not be downloaded. Make sure every link in image_urls is publicly downloadable.) or read (A reference image could not be read. It may be corrupted or in an unsupported format.). Usually found when generation runs, after freezing: all of it is returned (charged "0.00", refunded equal to frozen); within a few seconds of creating the task, nothing frozen. | Replace the image error.message points to and create the task again. |
video_too_short / video_too_long | The source video is shorter than 4 or longer than 60 seconds (The video duration must be between 4 and 60 seconds.). Within a few seconds of creating the task, nothing frozen. | Use a video of 4–60 seconds and create the task again. |
invalid_request | The source video is longer than 15 seconds and you sent image_urls (image_urls is only allowed when the source video is at most 15 seconds long.). Within a few seconds of creating the task, nothing frozen. | Leave out image_urls, or trim the video to 15 seconds or less. |
insufficient_balance | Your available balance can't cover the cost worked out from the source video. Within a few seconds of creating the task, nothing frozen. | Top up, then create the task again. |
spend_limit_exceeded | This task's cost would go over one of the key's spend limits. Within a few seconds of creating the task, nothing frozen. | Raise the limit in Console → API keys, or wait for it to reset, then create the task again. |
content_policy | The video or a reference image breaks the content policy. | Resubmitting it as is fails again: use different media. |
generation_failed | Generation failed. | Create it again later. |
queue_timeout / internal_error | Not started within 30 minutes / an error on our side. | Create it again later. |
Every failed task has its frozen amount returned in full and billing.charged "0.00". Branch on error.type: the quoted messages are for display and may change, see Messages.