Upscale Video
Upscale a video to 1080p, 2k or 4k with one of two upscalers, priced from the source video.
Upscale Video (product upscale_video) upscales a video to 1080p, 2k or 4k, with its audio. There are two upscalers, bytedance and topaz, with their own parameters and pricing. One task makes one video, priced from the source video.
Create a task
https://api.nanoapi.ai/v1/tasksUse the shared create endpoint with model: "upscale-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 | upscale-video. |
video_url | string | Yes | The video to upscale, at most 180 seconds. See Media. |
provider | string | Yes | The upscaler: bytedance or topaz. They take different parameters and are priced differently. |
resolution | string | Yes | bytedance: 1080p, 2k or 4k. topaz: 1080p or 4k. Sets the price. |
fps | integer | bytedance: yes | Output frame rate: 24, 30 or 60. Sets the price: 60 doubles it. Only for bytedance; sending it with topaz returns 400. |
preset | string | bytedance: yes | Pick by the content: common (general), aigc (AI-generated video), short_series (short drama), ugc (user-shot video) or old_film (old film). Doesn't change the price. Only for bytedance; sending it with topaz returns 400. |
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. |
With bytedance, to 1080p at 24 fps:
curl https://api.nanoapi.ai/v1/tasks \
-H "Authorization: Bearer $NANOAPI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9b4d2f7a-6e1c-4c8b-a3f5-8d0e6b2a9c47" \
-d '{
"model": "upscale-video",
"video_url": "https://example.com/clip.mp4",
"provider": "bytedance",
"resolution": "1080p",
"fps": 24,
"preset": "common"
}'With topaz, to 4k (no fps or preset), the body is:
{
"model": "upscale-video",
"video_url": "https://example.com/clip.mp4",
"provider": "topaz",
"resolution": "4k"
}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 can't be read): go by what retrieving the task or the webhook tells you.
{
"code": 200,
"message": "success",
"data": {
"object": "task",
"id": "tk_01jbm6e8g1j4k7n0s3v6y9c2d5",
"model": "upscale-video",
"status": "queued",
"error": null,
"created_at": "2026-11-03T08:30:00Z"
}
}Get the result
Measured from creation to the end: a 10-second video to 1080p takes about 2 minutes with topaz and 2.5 minutes with bytedance; long videos take much longer, and everything is 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 bytedance request above, once it has succeeded (other fields left out):
{
"status": "succeeded",
"input": {
"video_url": "https://example.com/clip.mp4",
"provider": "bytedance",
"resolution": "1080p",
"fps": 24,
"preset": "common"
},
"num_outputs": 1,
"outputs": [
{
"index": 0,
"type": "video",
"url": "https://cdn.nanoapi.ai/h/tk_01jbm6e8g1j4k7n0s3v6y9c2d5_1_1793694600.mp4",
"preview_url": "https://cdn.nanoapi.ai/h/tk_01jbm6e8g1j4k7n0s3v6y9c2d5_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 | Upscale Video |
|---|---|
outputs | One item. |
outputs[0].type | video. |
outputs[0].url | The upscaled video with its audio track, for downloading. bytedance: an h264 mp4. topaz: HEVC (h265), which some browsers can't play: show preview_url on web pages instead. |
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. |
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
The video is passed as a public http(s) URL in video_url. There's no upload endpoint: host the file 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) | |
|---|---|
| Length | At most 180 seconds (limits.max_source_video_seconds in GET /v1/products) |
| If there's a problem | Read within a few seconds of creating the task: if it can't be read, the task fails with invalid_media; over 180 seconds, with video_too_long. Nothing is frozen. |
Keep the link reachable until the task finishes, including links that expire.
Tested lengths
Under 180 seconds isn't a guarantee. These are the lengths that have been upscaled successfully; longer videos may fail, with the frozen amount returned in full (invalid_media, video_too_long, invalid_request or generation_failed):
provider | 1080p | 2k | 4k |
|---|---|---|---|
bytedance | Up to 10 seconds | Not tested | Up to 3 seconds |
topaz | Up to 30 seconds | – | Up to 10 seconds |
Pricing
bytedance: cost = price per second (resolution, fps) × length of the source video
topaz: cost = per-task amount + ceil(frames × price per frame ÷ price step) × price stepLoading the current prices…
bytedance: the length of the source video's video track, not rounded.fps60doubles the price per second;presetdoesn't change it.topaz: the source video's frame count, known only once the platform reads the video. The frame part is rounded up to a whole multiple ofprice_step(on thetopazrows ofGET /v1/products). Compute in integer micro-dollars (amount × 1,000,000), not floats: a floating-point division can land just above a whole number (0.07 / 0.01is7.000000000000001in JavaScript) and round up one step too many.- 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.
Examples: a 10-second video upscaled with bytedance to 1080p at 24 fps (the first request above) costs 10 × the 1080p / 24 fps price per second. A 10-second, 24 fps video (240 frames) upscaled with topaz to 4k costs the 4k per-task amount plus 240 × the 4k price per frame, rounded up to a multiple of price_step.
To show a price before submitting, read the video's length on your side (for topaz, estimate the frames as duration × frame rate) and apply the current prices from GET /v1/products: the upscale_video product has one prices row per provider, resolution and fps; the topaz rows have billing_unit frame, the per-task amount in base_price and the rounding step in price_step. Prices change, so don't hard-code them. It's an estimate: 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, provider or resolution is missing (Field required), or bytedance without fps or preset (fps is required for provider bytedance). param names it. | Send every required parameter, see Create a task. |
400 | invalid_request | fps or preset sent with topaz (fps is only allowed for provider bytedance). | Remove it. |
400 | invalid_request | provider isn't bytedance / topaz, resolution isn't one the provider offers (Input should be '1080p' or '4k' for provider topaz), fps or preset isn't one of the values above, video_url isn't a public http(s) URL of at most 4096 characters, or num_outputs isn't 1. | Use an allowed value. |
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: the link doesn't open or has expired, isn't public, or isn't a video. Within a few seconds of creating the task, nothing frozen. It can also come from the generation service, after freezing (all of it is returned). | Use a public, direct link to the video and create the task again. |
video_too_long | The source video is longer than 180 seconds (The video duration must be at most 180 seconds.): within a few seconds of creating the task, nothing frozen. Or the generation service doesn't take a video this long (The video is too long for this model. Use a shorter video.): after freezing, all of it is returned. | Use a shorter video, see Tested lengths. |
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. |
invalid_request | The generation service didn't accept this video with these parameters, for example a video longer than the tested lengths. After freezing, all of it is returned. | Use a shorter video or a lower resolution. |
content_policy | The video breaks the content policy. | Resubmitting it as is fails again: use a different video. |
generation_failed | Generation failed. | Create it again later, or use a shorter video. |
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.