Extend a video

October 7, 2026 (October 22, 2026)

Table of contents

  1. Request Headers
  2. Request Body
  3. Responses
  4. Model
  5. Examples
  6. Try It

Continue a clip made by this API with 3 to 10 more seconds of Gemini Omni 1.1 Flash video. The prompt describes what happens in the new seconds. The result is the whole clip, the source followed by its continuation, with a new mediaId. A 3-second clip extended by 3 seconds comes back as one 6-second clip.

The source is a video mediaId from POST /videos, an earlier extend, POST /videos/upscale or POST /videos/edit. The job runs on the account that made it. One clip can be extended several times, each extend branching into its own new clip.

An extend may change the shape of the clip. With aspectRatio and resolution left out, the result keeps the source’s. Set them to turn a landscape clip into a portrait one, or a 720p clip into 1080p, and the whole returned clip has the new shape.

Extending a 1080p clip works, but faces get softer at every join. If the finished clip will be 20 seconds or less, stay at 720p and upscale it at the end:

In our tests a 1080p clip behaves like a 720p clip that was upscaled, and it takes about twice as long to make. When you extend or edit a 1080p clip, Google seems to work on a 720p copy and then upscale the result again. Each round makes faces and small details softer. So:

  • Make, extend and edit your clips at 720p.
  • When the clip is finished, upscale it once with POST /videos/upscale. Upscale takes clips of up to 20 seconds.
  • Pick 1080p from the start for a single clip that you won’t extend or edit. A clip longer than 20 seconds can’t be upscaled, so for a long 1080p clip, extend at 720p and set resolution: "1080p" on the last extend only, or extend at 1080p throughout and accept softer faces.

Google Flow extends Veo clips only, so this is the way to extend an Omni clip.

The result carries Google’s visible Gemini ✦ (see Watermark). Our watermark remover does not work on extended clips: the model repaints the source’s star into the picture, so a faint diamond stays even after the new stamp is removed.

How long a clip can get

Google keeps about the first 31 seconds of the source. Chains of extends work up to 40 seconds, for example 10 + 10 + 10 + 10, and each step returns the whole clip. Extending a longer clip returns its first ~31 seconds plus the new seconds and drops the rest of the source: in our tests a 40-second clip extended by 10 came back as 41 seconds, and by 4 as 35 seconds. So the longest clip is about 41 seconds.

An extend takes longer as the clip grows, about 65, 78 and 94 seconds for results of 20, 30 and 40 seconds. That is close to the 100 seconds a sync request waits, so run long extends with async: true.

Cost

An extend uses only the seconds it adds, its duration, from the account’s monthly Vids video allowance, whatever the length of the source. A request Google refuses uses nothing.

Sync, async and webhooks

  • Every generation runs as a job on our side, with up to 5 minutes for Google’s answer, so a slow answer is never lost.
  • Sync (the default): the POST waits for the job and answers 200 with the finished job record, or the job’s error code. A video usually takes 20 to 100 seconds, an image about 10. If the job is still running after about 100 seconds, the POST answers 202 with the job as it stands: fetch the result with GET /jobs/jobid. Always check status: only completed has a result.
  • Async: pass async: true. The POST answers 202 at once with the job in status: "processing". Poll GET /jobs/jobid or wait for the webhook.
  • replyUrl receives one POST of the final job record, as JSON, when the job completes or fails, for sync and async requests alike. It does not make a request async.
  • Each account runs at most maxJobs jobs at a time (default 3, range 1 to 10, set with POST /accounts). GET /jobs shows what is running. Job records are kept for 7 days.

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

Request Headers
Authorization: Bearer {API token}
Content-Type: application/json
# Alternatively you can use multipart/form-data
# Content-Type: multipart/form-data
Request Body
{
  "mediaId": "user:[email protected]:eyJ1IjoiaHR0…",
  "prompt": "Camera tilts up to the rainy sky",
  "duration": 3,
  "aspectRatio": "portrait",
  "resolution": "1080p",
  "async": true,
  "replyUrl": "https://your-domain.com/webhook",
  "replyRef": "boat-extend-1"
}
  • mediaId is required, the video mediaId of the clip to extend, from a finished job’s result.
  • prompt is required, what happens in the added seconds.
    Maximum length: 5000 characters.
  • duration is optional, the number of seconds to add.
    Range: 3 to 10. Default: 8.
  • aspectRatio is optional, the shape of the returned clip.
    Supported values: landscape, portrait. Default: the source clip’s.
  • resolution is optional, the resolution of the returned clip.
    Supported values: 720p, 1080p. Default: the source clip’s.
  • async is optional, true to answer 202 at once and run the job in the background. Default: false.
  • replyUrl is optional, a public http(s) URL that receives one POST of the job record when the job completes or fails. It works with sync and async requests.
    Callback body has the same JSON shape as GET /jobs/jobid response.
    Maximum length: 1024 characters.
  • replyRef is optional, your own reference echoed back in the job record.
    Maximum length: 1024 characters.
Responses
  • 200 OK — sync mode. Here a 3-second landscape 720p clip was extended by 3 seconds into a 6-second portrait 1080p clip, charged 3 seconds.

    {
      "jobid": "j1007132808631117470v-u12345-email:[email protected]:google-vids",
      "type": "video",
      "mode": "extend",
      "email": "[email protected]",
      "status": "completed",
      "created": "2026-10-07T06:26:11.800Z",
      "request": {
        "mediaId": "user:[email protected]:eyJ1IjoiaHR0…",
        "prompt": "Camera tilts up to the rainy sky",
        "duration": 3,
        "aspectRatio": "portrait",
        "resolution": "1080p"
      },
      "updated": "2026-10-07T06:27:52.191Z",
      "completed": "2026-10-07T06:27:52.191Z",
      "result": {
        "mediaId": "user:[email protected]:eyJ1IjoiaHR0…",
        "width": 1080,
        "height": 1920,
        "duration": 6,
        "resolution": "1080p",
        "aspectRatio": "portrait",
        "model": "/flix/generate_videos_omni_extend_psq/v1",
        "quota": {
          "video": {
            "limit": 10000,
            "left": 9627,
            "resetAt": "2026-11-01T07:00:00.000Z"
          }
        },
        "elapsedMs": 100336
      }
    }
    
  • 202 Accepted — the job is running: with async: true at once, in sync mode after about 100 seconds of waiting. Fetch the result with GET /jobs/jobid (or wait for the webhook).

  • 400 Bad Request — a parameter is missing or invalid, or Google rejected the request.

    {
      "error": "Parameter mediaId is not a valid video id",
      "code": 400
    }
    
    {
      "error": "Parameter duration (11) is more than 10",
      "code": 400
    }
    
  • 401 Unauthorized

    Invalid API token.

    {
      "error": "useapi.net ⁝ Unauthorized",
      "code": 401
    }
    
  • 403 Forbidden — the mediaId was issued to a different API token, or the account has Google Workspace smart features turned off.

    {
      "error": "video id does not belong to this API token",
      "code": 403
    }
    
  • 404 Not Found — the account that made the clip is no longer connected.

    {
      "error": "Account [email protected] (from the video id) is not configured",
      "code": 404
    }
    
  • 422 Unprocessable Content — Google refused the prompt or the clip. Nothing was charged. The body is the failed job record, see POST /videos.

  • 429 Too Many Requests — the account is running maxJobs jobs, the month’s video allowance is used up, or Google is limiting the account for a minute.

    {
      "error": "Account [email protected] is running 3 of 3 jobs (maxJobs). Wait for one to finish, or raise maxJobs with POST /accounts",
      "code": 429
    }
    

    Once Google refuses a job because the account’s monthly video allowance is used up, the API keeps Google’s reset time on the account (exhaustedUntil in GET /accounts) and gives it no video jobs until then. A request that must run on that account is answered at once with 429, with no job created and no call to Google. retryAt is when to try again. If the account’s plan changed, GET /accounts/email reads its allowance again and lets it take jobs right away.

    {
      "error": "Account [email protected] has used up this month's Google Vids video allowance. It resets at 2026-11-01T07:00:00.000Z. If its plan changed, call GET /accounts/[email protected] to refresh it",
      "code": 429,
      "retryAt": "2026-11-01T07:00:00.000Z"
    }
    

    A job that Google itself refuses for its allowance (the first refusal, or a job that needs more than the account has left) fails with error.code 429, error.retryAt set to the reset time, and error.quota with what was left (limit, left, resetAt). An account with some allowance left still takes smaller jobs.

  • 503 Service Unavailable — Google answered with a server error. We saw this once when extending a 40-second clip. Retry, or extend a shorter clip.

    {
      "error": {
        "code": 503,
        "message": "Google error: Internal error encountered."
      }
    }
    

    The body is the failed job record, shortened here to its error.

  • 504 Gateway Timeout — Google did not answer within 5 minutes: run it again.

    {
      "error": {
        "code": 504,
        "message": "Google did not answer in time (Google did not answer within 900 s). Google may still finish it and charge it, but the result cannot be retrieved: run it again"
      }
    }
    

    The body is the failed job record, shortened here to its error.

  • 596 Account Error

    Google reported the account as signed out. While we re-check it, the answer is the first message below. Retry in about a minute. If it stays signed out, the account is paused, you receive an email, and the answer becomes the second message until you re-add it via Setup Google Vids.

    {
      "error": "Google reported this account as signed out. We are re-checking it — please retry in about a minute. If it stays signed out, you will receive a re-add email.",
      "code": 596
    }
    
    {
      "error": "Account [email protected]: Google signed this account out. Re-add it at https://useapi.net/docs/start-here/setup-google-vids",
      "code": 596
    }
    
Model

The job record, mode: "extend". GET /jobs/jobid and the replyUrl webhook return the same shape. result.duration is the length of the whole returned clip.

{ // TypeScript, all fields are optional
  jobid: string                  // j<time><random><v|i>-u<id>-email:<email>-bot:google-vids
  type: 'video' | 'image'
  mode?: 'text' | 'image' | 'ingredients' | 'extend' | 'upscale' | 'edit'   // video jobs only
  email: string                  // the account the job runs on
  status: 'processing' | 'completed' | 'failed'
  created: string                // ISO 8601, when the job was accepted
  updated?: string               // ISO 8601, the last status change
  completed?: string             // ISO 8601, when the job completed or failed
  request: Record<string, unknown>   // your request body as sent (ids as strings)
  replyUrl?: string
  replyRef?: string
  error?: {                      // status 'failed'
    code: number                 // 400 | 403 | 404 | 422 | 429 | 500 | 502 | 503 | 504 | 596, see GET /jobs/{jobid}
    message: string
    retryAt?: string             // code 429: ISO 8601, when to try again
    quota?: {                    // code 429 for allowance: Google's counter for this kind of job when it refused it
      limit: number              // monthly limit (seconds of video, or images)
      left: number               // what was left, 0 when used up
      resetAt: string            // ISO 8601, when the allowance resets
    }
  }
  result?: {                     // status 'completed'
    mediaId: string              // download it with GET /media/{mediaId}. A video: extend / upscale / edit it. An image: use it as startImage, referenceImage_N or avatar image
    width: number
    height: number
    duration?: number            // video: length of the whole clip in seconds
    resolution?: '720p' | '1080p'              // video
    aspectRatio?: 'landscape' | 'portrait'     // video
    model: string | null         // Google's backend path, e.g. /flix/generate_videos_omni_t2v_psq/v1
    quota: {                     // what the account has left after this job
      video?: { limit: number, left: number, resetAt: string }   // seconds, video jobs
      image?: { limit: number, left: number, resetAt: string }   // images, image jobs
    }
    elapsedMs: number            // how long Google took
  }
}
Examples
  • curl -X POST "https://api.useapi.net/v1/google-vids/videos/extend" \
       -H "Content-Type: application/json" \
       -H "Authorization: Bearer …" \
       -d '{
         "mediaId": "user:[email protected]:eyJ1IjoiaHR0…",
         "prompt": "The paper boat spins and sails into a puddle",
         "duration": 10,
         "async": true
       }'
    
  • const token = "API token";
    const mediaId = "mediaId of a clip made by this API";
    const apiUrl = "https://api.useapi.net/v1/google-vids";
    const headers = { "Content-Type": "application/json", "Authorization": `Bearer ${token}` };
    
    const response = await fetch(`${apiUrl}/videos/extend`, {
      method: "POST",
      headers,
      body: JSON.stringify({ mediaId, prompt: "The paper boat spins and sails into a puddle", duration: 10, async: true })
    });
    let job = await response.json();
    while (job.status === "processing") {
      await new Promise(r => setTimeout(r, 10000));
      job = await (await fetch(`${apiUrl}/jobs/${encodeURIComponent(job.jobid)}`, { headers })).json();
    }
    console.log("job", job.result ?? job.error);
    
  • import time
    import requests
    from urllib.parse import quote
    
    token = "API token"
    mediaId = "mediaId of a clip made by this API"
    apiUrl = "https://api.useapi.net/v1/google-vids"
    headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
    
    data = {"mediaId": mediaId, "prompt": "The paper boat spins and sails into a puddle", "duration": 10, "async": True}
    job = requests.post(f"{apiUrl}/videos/extend", headers=headers, json=data).json()
    while job.get("status") == "processing":
        time.sleep(10)
        job = requests.get(f"{apiUrl}/jobs/{quote(job['jobid'], safe='')}", headers=headers).json()
    print(job.get("result") or job.get("error"))
    
Try It