Extend Videos

January 29, 2026 (November 6, 2026)

Table of contents

  1. Request Headers
  2. Request Body
  3. Responses
  4. Model
  5. Missing media URLs
    1. How to get the link
  6. Examples
  7. Try It

Extend a video with a new prompt: Omni 1.2 Flash continues the clip by 3 to 10 seconds and returns one clip that holds the whole source plus the new seconds. Extend the result again to keep going.

  • Extending with Omni 1.2 Flash requires a paid Google AI plan (Plus, Pro or Ultra) with remaining credits. A free Google account gets 400, and can still extend with a Veo model (see below).
  • Any video shorter than 30 seconds can be extended: one generated with any model (Veo included), uploaded, or already extended. A video of 30 seconds or more gets 400, so the longest result is just under 40 seconds.
  • The extension keeps the source’s aspect ratio (landscape or portrait). Its resolution is the source’s own unless you set resolution.
  • It costs the same as an Omni 1.2 Flash generation of the added length, on every plan: at 720p 3 credits a second (10 s = 30), at 360p 1 credit a second (3 s = 3).
  • Images, characters and voices can steer the extension, including a voice for new dialogue (see Request Body).
  • Google is giving Omni 1.2 Flash to accounts over several days from November 5, 2026. On an account that doesn’t have it yet the request returns 400, and a Veo model still extends there (see below).

The Veo models still extend when you set model to one of them, the way this endpoint worked before Omni 1.2 Flash: Google adds an ~7 s segment that is returned on its own and overlaps the source by ~1 second, so join the two with POST /videos/concatenate and trimStart. A Veo extension takes no duration, resolution or references.

https://api.useapi.net/v1/google-flow/videos/extend

Request Headers

Authorization: Bearer {API token}
Content-Type: application/json

Request Body

{
  "mediaGenerationId": "user:12345-email:6a6f...-video:CAMaJDMx...",
  "prompt": "The magician runs after the glowing card through the rain, calling out @referenceAudio_1: \"Come back here! You are the ace of hearts!\"",
  "duration": 10,
  "referenceAudio_1": "Charon"
}
  • mediaGenerationId is required. The mediaGenerationId of a video shorter than 30 seconds: one from POST /videos (any model), from this endpoint, or uploaded via POST /assets/email. The extension runs on the account that holds it.
  • prompt is required. What happens next. It can name the references inline with @referenceImage_1..3, @character_1..3 and @referenceAudio_1..3, the same inline markers as POST /videos.
  • model is optional (default: omni-flash-1.2). Supported values: omni-flash-1.2, veo-3.1-fast, veo-3.1-quality, veo-3.1-lite, veo-3.1-lite-low-priority (Ultra $199 only, no credits, lower priority). A Veo model is used only when you set it here, and works as described above. On an account invited to an Ultra $199 family plan, veo-3.1-lite-low-priority isn’t available — only the plan owner’s account has it — and the request returns 400.
  • duration is optional (omni-flash-1.2 only), the seconds to add: any whole number from 3 to 10 (default: 8).
  • resolution is optional (omni-flash-1.2 only), 360p or 720p (default: the source’s resolution). 360p costs a third of 720p.
  • referenceImage_1 to referenceImage_3 are optional (omni-flash-1.2 only), mediaGenerationId of images from POST /assets/email on the same account.
  • character_1 to character_3 are optional (omni-flash-1.2 only), characters from POST /characters. Each character’s images count toward the same limit of 3 images.
  • referenceAudio_1 to referenceAudio_3 are optional (omni-flash-1.2 only), voices for new dialogue: a system voice name (one of the 30 presets listed on POST /videos) or a user voice from POST /voices.
  • count is optional. Number of variations to generate (1-4, default: 1). Each variation uses a different seed, and on omni-flash-1.2 each one is a full clip of the source plus its own extension.
  • seed is optional. Random seed for reproducible results. If not specified, a random seed is generated. When count > 1, seeds are incremented (seed, seed+1, seed+2, …).
  • async is optional, enables fire-and-forget mode (default: false). When true, returns immediately with 201 Created and job metadata. Poll GET /jobs/jobId for completion status.
  • replyUrl is 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/jobId response.
  • replyRef is optional, custom reference string passed back in webhook callbacks.
  • captchaToken, captchaRetry, captchaOrder are optional, mutually exclusive captcha parameters. See Captcha Parameters for details.

Responses

  • 200 OK

    Video extended successfully. All video data is in the media[] array. With omni-flash-1.2 each item is one clip of the source plus the extension: in this example a 10 s clip extended by 10 s, so length is 20s.

    {
      "jobId": "j1737312345678v-u12345-email:jo***@gmail.com-bot:google-flow",
      "media": [
        {
          "name": "a1d95d21-75d2-482d-a354-14ef8802ce66",
          "projectId": "9f63078c-...redacted...",
          "workflowId": "fa986834-...redacted...",
          "workflowStepId": "CAE",
          "mediaMetadata": {
            "createTime": "2026-11-06T02:13:29.851988Z",
            "mediaTitle": "The magician runs after the glowing card through the rain, calling out VoiceI: \"Come back here! You are the ace of hearts!\"",
            "requestData": { "videoGenerationRequestData": { "videoModelControlInput": { "videoModelName": "models/omni_1p5_pro_flow", "videoGenerationMode": "VIDEO_GENERATION_MODE_VIDEO_EXTENSION", "videoAspectRatio": "VIDEO_ASPECT_RATIO_LANDSCAPE", "videoResolution": "VIDEO_RESOLUTION_720P" } }, "clientPlatform": "CLIENT_PLATFORM_WEB" },
            "mediaStatus": { "mediaGenerationStatus": "MEDIA_GENERATION_STATUS_SUCCESSFUL" },
            "visibility": "PRIVATE",
            "mediaBlobSize": "10618058"
          },
          "video": {
            "generatedVideo": {
              "seed": 267051,
              "prompt": "The magician runs after the glowing card through the rain, calling out audio_0.wav : \"Come back here! You are the ace of hearts!\"",
              "model": "macaron_cookie_extend_10s",
              "baseImageMediaGenerationId": "",
              "isLooped": false,
              "aspectRatio": "VIDEO_ASPECT_RATIO_LANDSCAPE"
            },
            "dimensions": { "length": "20s" },
            "operation": { "name": "a1d95d21-...redacted..." }
          },
          "mediaGenerationId": "user:12345-email:6a6f...-video:a1d95d21-...redacted...",
          "videoUrl": "https://flow-content.google/video/a1d95d21-...redacted...?Expires=...",
          "thumbnailUrl": "https://flow-content.google/image/a1d95d21-...redacted...?Expires=..."
        }
      ],
      "operations": [],
      "remainingCredits": 847,
      "captcha": {
        "service": "AntiCaptcha",
        "taskId": "abc123...",
        "durationMs": 3500,
        "attempts": [
          {
            "service": "AntiCaptcha",
            "taskId": "abc123...",
            "durationMs": 3500,
            "success": true
          }
        ]
      }
    }
    
  • 201 Created

    Job created in async mode (async: true). Video extension is processing in the background.

    Use GET /jobs/jobId to poll for completion status.

    {
      "jobid": "j1737312345678v-u12345-email:jo***@gmail.com-bot:google-flow",
      "type": "video",
      "status": "created",
      "created": "2026-01-29T12:34:56.789Z",
      "request": {
        "async": true,
        "prompt": "The camera rises above the mountains as the sun sets behind them",
        "mediaGenerationId": "user:12345-email:6a6f...-video:CAMaJDMx..."
      },
      "response": {
        "captcha": {
          "service": "AntiCaptcha",
          "taskId": "14af1dbb-885c-4e25-8121-7a79489dfd0e",
          "durationMs": 5357
        }
      }
    }
    
  • 400 Bad Request

    Invalid request.

    {
      "error": "Error message",
      "code": 400
    }
    
  • 401 Unauthorized

    Invalid API token.

    {
      "error": "Unauthorized"
    }
    
  • 403 Forbidden

    Google rejected the captcha on every try. We retry with a fresh captcha as many times as you allow (captchaRetry, default 5, or the providers in captchaOrder) and return this 403 only when all of them were rejected. The top-level error starts with captcha_quality:, for example "captcha_quality: PUBLIC_ERROR_UNUSUAL_ACTIVITY after 5 attempts — see GET /accounts/captcha-stats summary.by_account".

    Two 403s are not a captcha problem, and we answer them after one try instead of retrying. PUBLIC_ERROR_MODEL_ACCESS_DENIED in the response means Google refused the model on that account, so raising captchaRetry or adding providers will not help. Check that the account’s plan includes the model. Since 2026-09-23 Google no longer offers veo-3.1-lite-low-priority to invited members of an Ultra $199 family plan — only the family manager’s account keeps it.

    PUBLIC_ERROR_USER_MINOR means Google treats the Google account as belonging to someone under 18, and Google Flow is not available to such accounts. The error says so in plain words. No captcha or retry changes it: use another Google account, or have this one’s age verified with Google first. With email omitted, the load balancer sets such an account aside for 1 hour, then tries it again.

    Nothing was created when this happens. Google checks the captcha before it starts generating, so no Google credits were used and you can resubmit straight away. Your captcha provider still bills each solve.

    The usual cause is the Google account, not the captcha provider. Google gives each account its own trust level. On a trusted account it accepts about 9 in 10 captchas, on a distrusted one about 3 in 10, whichever provider solved them. Most customers have a few distrusted accounts among many good ones.

    To see which accounts are affected, use GET /accounts/captcha-stats. Its summary.by_account shows, for each of your Google accounts, how many captchas were tried and what share Google accepted. GET account/stats with bot=google-flow also shows how many requests each account finished with each response code.

    • If one or a few accounts are far below the others, the API already sends them less work: when you leave out email, each captcha Google rejected on an account in the last 15 minutes lowers its chance of being picked, for generations and for image uploads. They still get some work, so an account that recovers gets its full share back. If you pin requests with email, move them to your other accounts yourself. Replace an account that stays low for more than a day or two.
    • If all your accounts drop at the same time, it’s the captcha provider. Configure at least two providers with POST /accounts/captcha-providers, and the API will start with whichever one Google accepts best.

    A higher captchaRetry gets more requests through on a distrusted account, but every extra try is another paid solve.

  • 404 Not Found

    Account not found or video not found.

    {
      "error": "Google Flow account [email protected] not found"
    }
    
  • 408 Request Timeout

    Video extension polling timeout (after 10 minutes).

    {
      "error": "Polling timeout"
    }
    
  • 429 Too Many Requests

    Google returns several RESOURCE_EXHAUSTED 429 variants. Check the reason field and respond accordingly:

    Reason Meaning Scope What to do Cooldown
    PUBLIC_ERROR_
    USER_REQUESTS_THROTTLED
    Google is throttling the account (the token was fine). Uploads and generation are metered separately All models on the account — or uploads only, when POST /assets triggered it Run fewer parallel requests, or switch accounts. From POST /assets, retrying the same one rarely clears it ~30 min. From POST /assets, treat it as a minimum — it can persist
    PUBLIC_ERROR_
    USER_QUOTA_REACHED
    Account’s overall Flow quota cap All models on the account Add accounts, or omit email to load-balance ~30 min
    PUBLIC_ERROR_
    PER_MODEL_DAILY_QUOTA_REACHED
    Per-model daily quota Only that model, on that account Switch model, or wait Next UTC midnight
    PUBLIC_ERROR_
    UNUSUAL_ACTIVITY_TOO_MUCH_TRAFFIC
    Google rejected the captcha on every try (we already retried). Usually a distrusted Google account, sometimes the provider Per request Check which accounts are affected in captcha-stats. Rest or replace those accounts. Add a second provider if all accounts drop together 60s

    Every 429 returns the cooldown two ways — an HTTP Retry-After: <seconds> header and a body retryAfter: <ISO timestamp> field. Honor it before retrying.

    When email is omitted, the first three reasons quarantine the account and the load balancer routes around it. If every account is quarantined for the model you get 429 with error: "no_eligible_account". Routing details: GET /jobs › Load Balancing Algorithm.

    Captcha-quality failures prefix the top-level error with captcha_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.

  • 503 Service Unavailable

    503 has 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 error is prefixed Captcha 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 extension completed. Returns extended video data with signed URLs.

    {
      jobId: string                              // Job identifier
      media: Array<{                             // Preferred — use this instead of operations
        name: string                             // Media identifier (raw UUID)
        projectId: string
        workflowId?: string
        workflowStepId?: string                  // e.g. "CAE"
        mediaMetadata: {
          createTime?: string                    // ISO 8601 timestamp
          mediaTitle?: string                    // Extension prompt
          requestData?: object                   // Full Google Flow request payload
          mediaStatus: {
            mediaGenerationStatus: string        // MEDIA_GENERATION_STATUS_SUCCESSFUL | FAILED
            error?: { code: number; message: string }
          }
          visibility?: string
          mediaBlobSize?: number                 // Size of the MP4 in bytes
        }
        video?: {
          generatedVideo: {
            seed: number
            prompt?: string                      // Extension prompt
            model: string                        // macaron_cookie_extend_<n>s[_360p] (omni-flash-1.2) | veo_3_1_extend_* (Veo)
            baseImageMediaGenerationId?: string  // Source video media ID
            isLooped: boolean
            aspectRatio: string                  // VIDEO_ASPECT_RATIO_LANDSCAPE | PORTRAIT
          }
          dimensions?: { length?: string }       // Clip length, e.g. "20s" (omni-flash-1.2: source + extension)
          operation: { name: string }
        }
        mediaGenerationId: string                // Encoded reference ID for use in subsequent API calls
        videoUrl?: string                        // Signed video download URL (MP4, valid ~6h)
        thumbnailUrl?: string                    // Signed thumbnail download URL (JPEG, valid ~6h)
      }>
      operations: []                             // Always empty on this endpoint — read media[]
      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/jobId response.

    {
      jobid: string                              // Job identifier
      type: 'video'                              // Job type
      status: 'created'                          // Job status
      created: string                            // ISO 8601 timestamp
      request: {
        async: true
        prompt: string                           // Extension prompt
        mediaGenerationId: string                // Source video reference
        model?: string                           // "omni-flash-1.2" (default) or a Veo model
        duration?: number                        // 3-10, omni-flash-1.2
        resolution?: string                      // "360p" | "720p", omni-flash-1.2
        seed?: number
        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.
}

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)
    
    # Extend the video
    curl -X POST \
         -H "Authorization: Bearer YOUR_API_TOKEN" \
         -H "Content-Type: application/json" \
         -d "{\"mediaGenerationId\": \"$MEDIA_ID\", \"prompt\": \"The camera rises above the mountains as the sun sets behind them\"}" \
         "https://api.useapi.net/v1/google-flow/videos/extend" > extend.json
    
    # Extract extended video URL and download
    VIDEO_URL=$(jq -r '.media[0].videoUrl' extend.json)
    curl "$VIDEO_URL" --output extended_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;
    
    // Extend the video
    const extendResponse = await fetch('https://api.useapi.net/v1/google-flow/videos/extend', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        mediaGenerationId,
        prompt: 'The camera rises above the mountains as the sun sets behind them'
      })
    });
    
    const extended = await extendResponse.json();
    // Prefer media[] over deprecated operations[]
    const videoUrl = extended.media[0].videoUrl;
    
    // Download extended video (Node.js)
    const videoResponse = await fetch(videoUrl);
    const videoBuffer = await videoResponse.arrayBuffer();
    require('fs').writeFileSync('extended_video.mp4', Buffer.from(videoBuffer));
    console.log('Extended 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']
    
    # Extend the video
    extend_response = requests.post(
        'https://api.useapi.net/v1/google-flow/videos/extend',
        headers=headers,
        json={
            'mediaGenerationId': media_generation_id,
            'prompt': 'The camera rises above the mountains as the sun sets behind them'
        }
    )
    extended = extend_response.json()
    # Prefer media[] over deprecated operations[]
    video_url = extended['media'][0]['videoUrl']
    
    # Download extended video
    video_response = requests.get(video_url)
    with open('extended_video.mp4', 'wb') as f:
        f.write(video_response.content)
    print('Extended video saved!')
    

Try It