Upscale Videos
January 20, 2026 (July 27, 2026)
Table of contents
Upscale a previously generated video to 1080p or 4K resolution.
- Currently, video upscaling requires a paid Google AI subscription or remaining credits.
- Currently,
4Kresolution requires a Google AI Ultra subscription. Other paid accounts can only use1080presolution. 1080pupscaling is free.4Kupscaling currently costs 50 credits.- Video upscaling typically completes within 30-60 seconds, with 4K taking a few minutes.
- Upscaling the same video twice returns a cached result (no additional credits consumed). Use the video URL from the first upscale response.
https://api.useapi.net/v1/google-flow/videos/upscale
Request Headers
Authorization: Bearer {API token}
Content-Type: application/json
# Alternatively you can use multipart/form-data
# Content-Type: multipart/form-data
API tokenis required, see Setup useapi.net for details.
Request Body
{
"mediaGenerationId": "user:12345-email:6a6f...-video:CAMaJDMx...",
"resolution": "1080p"
}
mediaGenerationIdis required. ThemediaGenerationIdfrom a previously generated video via POST /videos.resolutionis optional. Target resolution for upscaling (default:1080p).
Supported values:1080p,4K.asyncis optional, enables fire-and-forget mode (default:false).
Whentrue, returns immediately with201 Createdand job metadata. Poll GET /jobs/jobIdfor completion status.replyUrlis optional, webhook URL for job status callbacks.
Receives POST requests with job status updates (created,started,completed,failed). The JSON payload shape matches GET /jobs/jobIdresponse.replyRefis optional, custom reference string passed back in webhook callbacks.captchaToken,captchaRetry,captchaOrderare optional, mutually exclusive captcha parameters. See Captcha Parameters for details.
Responses
-
Video upscaled successfully. Both
operations[](legacy, withfifeUrl) andmedia[](current, withvideoUrl) are populated.{ "jobId": "j1737312345678v-u12345-email:jo***@gmail.com-bot:google-flow", "operations": [ { "operation": { "name": "2fefd089-...redacted..._upsampled", "metadata": { "@type": "type.googleapis.com/google.internal.labs.aisandbox.v1.Media", "name": "2fefd089-...redacted..._upsampled", "video": { "seed": 0, "model": "veo_3_1_upsampler_1080p", "aspectRatio": "VIDEO_ASPECT_RATIO_LANDSCAPE", "isLooped": false, "upsampleResolution": "VIDEO_UPSAMPLE_RESOLUTION_1080P", "fifeUrl": "https://flow-content.google/video/2fefd089-..._upsampled?Expires=...", "servingBaseUri": "https://flow-content.google/image/2fefd089-..._upsampled?Expires=...", "mediaGenerationId": "user:12345-email:6a6f...-video:2fefd089-..._upsampled", "mediaVisibility": "PRIVATE" } } }, "sceneId": "", "status": "MEDIA_GENERATION_STATUS_SUCCESSFUL", "mediaGenerationId": "user:12345-email:6a6f...-video:2fefd089-..._upsampled" } ], "media": [ { "name": "2fefd089-...redacted..._upsampled", "projectId": "9f63078c-...redacted...", "workflowId": "66612805-...redacted...", "workflowStepId": "CAI", "mediaMetadata": { "createTime": "2026-05-20T23:31:41.145235Z", "requestData": { "videoGenerationRequestData": { "...": "..." }, "clientPlatform": "CLIENT_PLATFORM_WEB" }, "mediaStatus": { "mediaGenerationStatus": "MEDIA_GENERATION_STATUS_SUCCESSFUL" }, "visibility": "PRIVATE" }, "video": { "generatedVideo": { "seed": 0, "model": "veo_3_1_upsampler_1080p", "baseImageMediaGenerationId": "", "isLooped": false, "aspectRatio": "VIDEO_ASPECT_RATIO_LANDSCAPE", "upsampleMetadata": { "videoUpsampleResolution": "VIDEO_UPSAMPLE_RESOLUTION_1080P" } }, "dimensions": { "length": "0s" }, "operation": { "name": "2fefd089-..._upsampled" } }, "mediaGenerationId": "user:12345-email:6a6f...-video:2fefd089-..._upsampled", "videoUrl": "https://flow-content.google/video/2fefd089-..._upsampled?Expires=...", "thumbnailUrl": "https://flow-content.google/image/2fefd089-..._upsampled?Expires=..." } ], "remainingCredits": 18760, "captcha": { "service": "AntiCaptcha", "taskId": "abc123...", "durationMs": 3500, "attempts": [ { "service": "AntiCaptcha", "taskId": "abc123...", "durationMs": 3500, "success": true } ] } } -
Job created in async mode (
async: true). Video upscaling is processing in the background.Use GET /jobs/
jobIdto poll for completion status.{ "jobid": "j1737312345678v-u12345-email:jo***@gmail.com-bot:google-flow", "type": "video", "status": "created", "created": "2026-01-19T12:34:56.789Z", "request": { "async": true, "mediaGenerationId": "user:12345-email:6a6f...-video:CAMaJDMx...", "resolution": "1080p" }, "response": { "captcha": { "service": "AntiCaptcha", "taskId": "14af1dbb-885c-4e25-8121-7a79489dfd0e", "durationMs": 5357 } } } -
Invalid request.
{ "error": "Error message" } -
Invalid API token.
{ "error": "Unauthorized" } -
Request rejected by Google — the reCAPTCHA token was outright rejected (not just scored low). After internal retries with fresh tokens, the worker returns this. The top-level
errorfield carries acaptcha_quality:prefix so tooling can detect this failure class. IncreasecaptchaRetry(default 3) in the request body to 5 or higher, and configure additional providers via POST /accounts/captcha-providers socaptchaOrdercan cycle across them. If issues persist, contact your reCAPTCHA provider(s) support. -
Account not found or video not found.
{ "error": "Google Flow account [email protected] not found" } -
Video upscaling polling timeout (after 10 minutes).
{ "error": "Polling timeout" } -
Google returns several
RESOURCE_EXHAUSTED429 variants. Check thereasonfield and respond accordingly:Reason Meaning Scope What to do Cooldown PUBLIC_ERROR_USER_REQUESTS_THROTTLEDPer-user concurrency limit (the token was fine) All models on the account Run fewer parallel requests, retry later ~30 min PUBLIC_ERROR_USER_QUOTA_REACHEDAccount’s overall Flow quota cap All models on the account Add accounts, or omit emailto load-balance~30 min PUBLIC_ERROR_PER_MODEL_DAILY_QUOTA_REACHEDPer-model daily quota Only that model, on that account Switch model, or wait Next UTC midnight PUBLIC_ERROR_UNUSUAL_ACTIVITY_TOO_MUCH_TRAFFICCaptcha-provider issue — low reCAPTCHA score (worker already auto-retried) Per request Add more captcha providers + captchaOrder, or raisecaptchaRetry60s Every
429returns the cooldown two ways — an HTTPRetry-After: <seconds>header and a bodyretryAfter: <ISO timestamp>field. Honor it before retrying.When
emailis omitted, the first three reasons quarantine the account and the load balancer routes around it. If every account is quarantined for the model you get429witherror: "no_eligible_account". Routing details: GET /jobs › Load Balancing Algorithm.Captcha-quality failures prefix the top-level
errorwithcaptcha_quality:(e.g."captcha_quality: PUBLIC_ERROR_UNUSUAL_ACTIVITY_TOO_MUCH_TRAFFIC after 3 attempts") so you can detect them without parsing the nested body. -
503has two distinct causes — check the response body to tell them apart.A transient Google-side outage. Wait 5-10 seconds and retry.
{ "error": { "code": 503, "message": "Service temporarily unavailable.", "status": "UNAVAILABLE" } }A captcha-provider failure — the top-level
erroris prefixedCaptcha service failed:. The problem is your captcha provider (check its API key and balance), not Google. Don’t retry until the provider is back.{ "error": "Captcha service failed: ERROR_ZERO_BALANCE", "code": 503 }
Model
-
Video upscaling completed. Returns upscaled video data with signed URLs. Re-upscaling the same video returns
rawBytes(base64 video data) instead of URLs.{ jobId: string // Job identifier operations: Array<{ // Legacy shape, still populated for upscale (with fifeUrl) operation: { name: string metadata: { '@type': string name: string video: { seed: number model: string // veo_3_1_upsampler_1080p | veo_3_1_upsampler_4k aspectRatio: string // VIDEO_ASPECT_RATIO_LANDSCAPE | PORTRAIT isLooped: boolean upsampleResolution?: string // 'VIDEO_UPSAMPLE_RESOLUTION_1080P' | 'VIDEO_UPSAMPLE_RESOLUTION_4K' fifeUrl: string // Signed video URL (MP4) servingBaseUri: string // Signed thumbnail URL (JPEG) mediaGenerationId: string // Encoded reference ID for the upscaled video mediaVisibility: string } } } sceneId: string status: string // MEDIA_GENERATION_STATUS_SUCCESSFUL | FAILED mediaGenerationId: string }> media: Array<{ // Current shape — same fields as POST /videos name: string // Media identifier (raw UUID) projectId: string workflowId?: string workflowStepId?: string // e.g. "CAI" for upscale mediaMetadata: { createTime?: string // ISO 8601 timestamp requestData?: object // Full Google Flow request payload mediaStatus: { mediaGenerationStatus: string // MEDIA_GENERATION_STATUS_SUCCESSFUL | FAILED error?: { code: number; message: string } } visibility?: string } video?: { generatedVideo: { seed: number model: string // veo_3_1_upsampler_1080p | veo_3_1_upsampler_4k baseImageMediaGenerationId?: string // Source video media ID isLooped: boolean aspectRatio: string // VIDEO_ASPECT_RATIO_LANDSCAPE | PORTRAIT upsampleMetadata?: { videoUpsampleResolution: string } // VIDEO_UPSAMPLE_RESOLUTION_1080P | _4K } dimensions?: { length?: string } // Often "0s" for upscale (not meaningful) operation: { name: string } } mediaGenerationId: string // Encoded reference ID for use in subsequent API calls videoUrl?: string // Signed video download URL (MP4) thumbnailUrl?: string // Signed thumbnail download URL (JPEG) }> remainingCredits?: number captcha?: { // Captcha metadata service: string // "CapSolver" | "AntiCaptcha" | "YesCaptcha" | "CapMonster" | "SolveCaptcha" | "2Captcha" | "EzCaptcha" | "UserProvided" taskId?: string durationMs: number attempts: Array<{ service: string taskId?: string durationMs: number success: boolean error?: string }> } } -
Job created and processing in background. Structure matches GET /jobs/
jobIdresponse.{ jobid: string // Job identifier type: 'video' // Job type status: 'created' // Job status created: string // ISO 8601 timestamp request: { async: true mediaGenerationId: string // Original video reference resolution?: string // "1080p" | "4K" replyUrl?: string replyRef?: string } response: { captcha?: { // Present when captcha was used service: string // Provider or "UserProvided" taskId?: string // Absent for UserProvided durationMs: number } } } -
Error response structure.
{ jobId?: string // Present for job-related errors error: string // Error summary message code?: number // HTTP status code response?: { // API response with error details error?: { code: number message: string status: string details?: Array<{ '@type': string reason: string }> } } }
Missing media URLs
Media URL fields are optional and can be absent even when generation succeeded. When videoUrl, thumbnailUrl, fifeUrl, servingBaseUri, previewUrl or audioUrl is missing, the media itself is fine — only the download link could not be produced at that moment.
This is rare. Across the seven days of request logs we keep, it happened on one day only — twice, for about four hours and about forty minutes — and not at all on the other six. Google issues those links from an endpoint that limits how often it will answer a given network address, so it is Google limiting our outbound calls rather than useapi.net limiting you, and nothing you do triggers it. While it lasts it affects every account generating at that moment, then clears on its own. Your generation still completed and was charged as usual.
On a media item that generated successfully — mediaStatus.mediaGenerationStatus is MEDIA_GENERATION_STATUS_SUCCESSFUL — a missing URL means “not yet”, not “failed”, so read the field before using it and treat its absence as a retry condition. A media item that failed generation never carries a URL, and retrying will not produce one.
const url = media.videoUrl;
if (!url) {
// Not an error — the link is not available yet. See the options below.
}
How to get the link
Which route to use depends on whether you can wait.
If you are already polling the job, do nothing special. Completed video jobs re-resolve missing URLs automatically on GET /jobs/jobId, and the recovered link is saved, so a later poll returns it with no extra work on your side. This applies to video jobs created within the last 24 hours. Once the job is completed and a URL is still missing, leave at least a minute between polls — a job re-resolves at most once a minute, and the block clears only once traffic stops, so a tight retry loop lengthens the wait rather than shortening it.
If you hold a mediaGenerationId but no longer have the job, ask for that one media again with GET /assets/mediaGenerationId. That endpoint says which case you are in rather than leaving you to guess. A 503 with a Retry-After header means the link is coming and tells you how long to wait, so wait that long and ask again. A 404 means Google has no such media, and no amount of retrying will change it.
If you cannot wait, download the video through us with GET /assets/mediaGenerationId?raw=true. It reads the media over a different Google route that the block does not touch, so it works immediately and keeps working while the other two return nothing. It is video only.
Prefer the two link-based routes when you can. They hand you a link and you download straight from Google. The raw form moves the whole file through useapi.net on every call, which makes it a way out of a stuck download rather than your normal download path.
Characters and voices resolve their URLs as they are read, so simply requesting GET /characters/ref or GET /voices/ref again is enough. Wait a minute before retrying — a response that came back without its URL is held briefly, so an immediate repeat returns the same one.
None of this applies to images. Whenever an image URL is missing, POST /images puts the image itself in the response instead, base64-encoded in encodedImage — so you already have the picture and there is nothing to retry.
Examples
-
# First generate a video curl -X POST \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"prompt": "A serene mountain landscape at sunset"}' \ "https://api.useapi.net/v1/google-flow/videos" > generate.json # Extract mediaGenerationId from the response MEDIA_ID=$(jq -r '.media[0].mediaGenerationId' generate.json) # Upscale the video to 1080p curl -X POST \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"mediaGenerationId\": \"$MEDIA_ID\", \"resolution\": \"1080p\"}" \ "https://api.useapi.net/v1/google-flow/videos/upscale" > upscale.json # Extract upscaled video URL and download VIDEO_URL=$(jq -r '.media[0].videoUrl' upscale.json) curl "$VIDEO_URL" --output upscaled_video.mp4 -
const token = 'YOUR_API_TOKEN'; // First generate a video const generateResponse = await fetch('https://api.useapi.net/v1/google-flow/videos', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: 'A serene mountain landscape at sunset' }) }); const generated = await generateResponse.json(); // Prefer media[] over deprecated operations[] const mediaGenerationId = generated.media[0].mediaGenerationId; // Upscale the video const upscaleResponse = await fetch('https://api.useapi.net/v1/google-flow/videos/upscale', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ mediaGenerationId, resolution: '1080p' }) }); const upscaled = await upscaleResponse.json(); // Prefer media[] over deprecated operations[] const videoUrl = upscaled.media[0].videoUrl; // Download upscaled video (Node.js) const videoResponse = await fetch(videoUrl); const videoBuffer = await videoResponse.arrayBuffer(); require('fs').writeFileSync('upscaled_video.mp4', Buffer.from(videoBuffer)); console.log('Upscaled video saved!'); -
import requests token = 'YOUR_API_TOKEN' headers = { 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } # First generate a video generate_response = requests.post( 'https://api.useapi.net/v1/google-flow/videos', headers=headers, json={'prompt': 'A serene mountain landscape at sunset'} ) generated = generate_response.json() # Prefer media[] over deprecated operations[] media_generation_id = generated['media'][0]['mediaGenerationId'] # Upscale the video upscale_response = requests.post( 'https://api.useapi.net/v1/google-flow/videos/upscale', headers=headers, json={ 'mediaGenerationId': media_generation_id, 'resolution': '1080p' } ) upscaled = upscale_response.json() # Prefer media[] over deprecated operations[] video_url = upscaled['media'][0]['videoUrl'] # Download upscaled video video_response = requests.get(video_url) with open('upscaled_video.mp4', 'wb') as f: f.write(video_response.content) print('Upscaled video saved!')