Generate 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

Generate a video clip with Gemini Omni 1.1 Flash in Google Vids. Clips are 3 to 10 seconds long, at 720p or 1080p, landscape or portrait, and come with sound. Characters speak the lines you write in the prompt, and an avatar speaks them in its own voice.

A clip can start from one of three inputs:

mode in the job Inputs What Google does
text prompt only Text to video.
image startImage The clip opens on that image.
ingredients referenceImage_1..referenceImage_3 and/or avatar_1..avatar_3 Up to 3 references in all, images and avatars together. The clip shows the people, objects and places they picture.

startImage cannot be combined with references. An image is either an assetId from POST /assets or the mediaId of an image made with POST /images, which the API fetches from Google and uploads for you on that image’s account. The job record echoes the id you sent. Avatars come from POST /avatars or GET /avatars. All inputs of one request must live on the same account, and the job runs there.

The finished clip’s mediaId downloads it with GET /media/mediaId and continues it with POST /videos/extend, POST /videos/upscale and POST /videos/edit.

Cost

A clip uses its duration in seconds of the account’s monthly Vids video allowance, at the same rate for 720p and 1080p. An avatar or a reference adds nothing. A request Google refuses uses nothing. See Plans and monthly allowances.

Watermark

Google states that every Omni clip made in Vids carries an invisible SynthID watermark in its frames. Every MP4 we downloaded also has a visible Gemini ✦ sparkle in the bottom-right corner. Gemini’s media watermark setting does not reach Vids: with it switched off, new Vids clips still had the sparkle. The API returns Google’s file unchanged. Images have no visible mark. To take the sparkle off a downloaded clip, use the watermark remover in our GitHub examples: it reverses the blend in all four Vids formats and leaves SynthID untouched. It works on clips as generated and on their upscales, not on extended or edited clips, where the model repaints the star into the picture.

720p or 1080p

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.

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.

Prompt markers

You can place @-markers inside prompt that stand for the reference parameters you send, as in Google Flow. Markers are case-insensitive and optional.

Marker Stands for
@referenceImage_1..@referenceImage_3 the matching referenceImage_N body parameter
@avatar_1..@avatar_3 the matching avatar_N body parameter

Rules:

  • Each marker needs its body parameter. Otherwise the API returns 400 with 'avatar_2' was not provided in the request body.
  • A reference that the prompt does not mention is still used. Google sees it ahead of the prompt text.
  • The same marker may appear several times in one prompt.
  • An avatar’s name plays no part in the prompt. Two avatars may share a name and still appear in one clip, told apart by @avatar_1 and @avatar_2.
  • Other strings, such as @gmail.com or @image_1, stay plain prompt text.

Example, two avatars and a reference image:

{
  "prompt": "@avatar_1 and @avatar_2 sit at @referenceImage_1. @avatar_2 says: \"This coffee is perfect.\" @avatar_1 nods and says: \"Told you.\"",
  "avatar_1": "user:[email protected]:eyJnIjoiaDMy…",
  "avatar_2": "user:[email protected]:eyJnIjoiaDMy…",
  "referenceImage_1": "user:[email protected]:AVL_0qgATU91…",
  "duration": 6
}

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

Request Headers
Authorization: Bearer {API token}
Content-Type: application/json
# Alternatively you can use multipart/form-data
# Content-Type: multipart/form-data
Request Body
{
  "prompt": "@referenceImage_1 rides in the passenger seat of @referenceImage_2 driving along @referenceImage_3",
  "referenceImage_1": "user:[email protected]:AVL_0qjhK1Xn…",
  "referenceImage_2": "user:[email protected]:AVL_0qjOxEMQ…",
  "referenceImage_3": "user:[email protected]:AVL_0qgP9eeS…",
  "duration": 4,
  "aspectRatio": "landscape",
  "resolution": "720p",
  "async": true,
  "replyUrl": "https://your-domain.com/webhook",
  "replyRef": "car-ride-1"
}
  • prompt is required, what happens in the clip, including any spoken lines. It may carry @-markers, see Prompt markers.
    Maximum length: 5000 characters.
  • duration is optional, the clip length in seconds.
    Range: 3 to 10. Default: 8.
  • aspectRatio is optional.
    Supported values: landscape (16:9), portrait (9:16). Default: landscape.
  • resolution is optional.
    Supported values: 720p (1280×720 or 720×1280), 1080p (1920×1080 or 1080×1920). Default: 720p (recommended).
    Both cost the same. A 1080p clip takes about twice as long to generate. If you plan to extend or edit the clip, stay at 720p: see 720p or 1080p.
  • startImage is optional, an assetId from POST /assets or an image mediaId from POST /images. The clip opens on this image. Not accepted together with referenceImage_N or avatar_N.
  • referenceImage_1, referenceImage_2, referenceImage_3 are optional, the people, objects or places to show, each an assetId from POST /assets or an image mediaId from POST /images.
  • avatar_1, avatar_2, avatar_3 are optional, avatarIds from POST /avatars or GET /avatars. An avatar keeps its look and speaks in its own voice.
    References and avatars together: at most 3.
  • email is optional, the account to run on. When omitted, the account that holds the inputs is used, or, for a text-only clip, a healthy account with a free maxJobs slot and video seconds left this month.
  • 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.

Any other parameter returns 400 Parameter <name> not supported.

Responses
  • 200 OK — sync mode, the clip is ready.

    {
      "jobid": "j1007112632830970253v-u12345-email:[email protected]:google-vids",
      "type": "video",
      "mode": "ingredients",
      "email": "[email protected]",
      "status": "completed",
      "created": "2026-10-07T06:24:34.765Z",
      "request": {
        "prompt": "@referenceImage_1 rides in the passenger seat of @referenceImage_2 driving along @referenceImage_3",
        "referenceImage_1": "user:[email protected]:AVL_0qjhK1Xn…",
        "referenceImage_2": "user:[email protected]:AVL_0qjOxEMQ…",
        "referenceImage_3": "user:[email protected]:AVL_0qgP9eeS…",
        "duration": 4
      },
      "updated": "2026-10-07T06:25:01.482Z",
      "completed": "2026-10-07T06:25:01.482Z",
      "result": {
        "mediaId": "user:[email protected]:eyJ1IjoiaHR0…",
        "width": 1280,
        "height": 720,
        "duration": 4,
        "resolution": "720p",
        "aspectRatio": "landscape",
        "model": "/flix/generate_videos_omni_r2v_psq/v1",
        "quota": {
          "video": {
            "limit": 10000,
            "left": 9636,
            "resetAt": "2026-11-01T07:00:00.000Z"
          }
        },
        "elapsedMs": 26664
      }
    }
    
  • 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).

    {
      "jobid": "j1007154417508260291v-u12345-email:[email protected]:google-vids",
      "type": "video",
      "mode": "text",
      "email": "[email protected]",
      "status": "processing",
      "created": "2026-10-07T06:31:32.041Z",
      "request": {
        "prompt": "A snail crawling over a mossy log, macro",
        "duration": 3,
        "replyUrl": "https://your-domain.com/webhook",
        "replyRef": "snail-1"
      },
      "replyUrl": "https://your-domain.com/webhook",
      "replyRef": "snail-1"
    }
    
  • 400 Bad Request — a parameter is missing or invalid, inputs are combined in a way Vids does not take, or Google rejected the request. A failed sync job answers with its job record, other cases with:

    {
      "error": "'referenceImage_2' was not provided in the request body",
      "code": 400
    }
    
    {
      "error": "Use either startImage or referenceImage_1..3 / avatar_1..3, not both",
      "code": 400
    }
    
    {
      "error": "Up to 3 references in all: referenceImage_1..3 and avatar_1..3 together",
      "code": 400
    }
    
    {
      "error": "user:[email protected]:AVL_0qjhK1Xn… lives on [email protected], not [email protected]: every input of one job must come from the same account",
      "code": 400
    }
    
    {
      "error": "Parameter duration (11) is more than 10",
      "code": 400
    }
    

    An image mediaId that Google no longer serves:

    {
      "error": "referenceImage_1: Google answered HTTP 404 for this image (it may have expired). Generate it again or upload it with POST /assets",
      "code": 400
    }
    

    An avatar made in the Vids web app with one of Google’s older narrator voices cannot be used here. Make a new avatar with POST /avatars.

    {
      "error": "avatar_1 uses an older Vids narrator voice that the API cannot send. Make a new avatar with POST /avatars",
      "code": 400
    }
    
  • 401 Unauthorized

    Invalid API token.

    {
      "error": "useapi.net ⁝ Unauthorized",
      "code": 401
    }
    
  • 403 Forbidden — an input id was issued to a different API token, the account’s Google plan has no Vids video allowance, or the account has Google Workspace smart features turned off (the failed job record, error.code: 403).

    {
      "error": "image id does not belong to this API token",
      "code": 403
    }
    
  • 404 Not Found — the account named by email or by an input id is not connected.

    {
      "error": "Account [email protected] is not configured",
      "code": 404
    }
    
  • 422 Unprocessable Content — Google refused the prompt or the inputs. Nothing was charged. Change the prompt or the inputs and try again.

    {
      "jobid": "j1007123831345945676v-u12345-email:[email protected]:google-vids",
      "type": "video",
      "mode": "text",
      "email": "[email protected]",
      "status": "failed",
      "created": "2026-10-07T06:36:31.478Z",
      "request": {
        "prompt": "…",
        "duration": 4
      },
      "updated": "2026-10-07T06:36:41.176Z",
      "completed": "2026-10-07T06:36:41.176Z",
      "error": {
        "code": 422,
        "message": "Google refused this request (\"That request looks like it goes against our terms. Try asking something else.\"). Change the prompt or the inputs and try again."
      }
    }
    
  • 429 Too Many Requests — the account is running maxJobs jobs, every account is busy, 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"
    }
    

    When nothing ties the request to one account, the job goes to an account with allowance left. When every healthy account has used up its video allowance, retryAt is the earliest reset time:

    {
      "error": "All 3 healthy Google Vids accounts have used up this month's video allowance. The first one resets at 2026-11-01T07:00:00.000Z. If a plan changed, call GET /accounts/{email} 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.

    For example, a job that needed more seconds than the account had left:

    {
      "jobid": "j1007174201068993706v-u12345-email:[email protected]:google-vids",
      "type": "video",
      "mode": "text",
      "email": "[email protected]",
      "status": "failed",
      "created": "2026-10-07T06:32:10.218Z",
      "request": {
        "prompt": "A spinning top on a wooden table",
        "duration": 10
      },
      "updated": "2026-10-07T06:32:11.624Z",
      "completed": "2026-10-07T06:32:11.624Z",
      "error": {
        "code": 429,
        "message": "Account [email protected] has 6 of 10000 left this month in Google Vids, not enough for this request. It resets at 2026-11-01T07:00:00.000Z",
        "retryAt": "2026-11-01T07:00:00.000Z",
        "quota": {
          "limit": 10000,
          "left": 6,
          "resetAt": "2026-11-01T07:00:00.000Z"
        }
      }
    }
    
  • 503 Service Unavailable — Google answered with a server error, or the async job could not be queued. Retry shortly.

    {
      "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
    }
    

    Without email, the same code means no connected account is healthy and has a video allowance:

    {
      "error": "No healthy Google Vids account with a video allowance configured. Check GET /accounts",
      "code": 596
    }
    
Model

The job record. GET /jobs/jobid and the replyUrl webhook return the same shape. In sync mode a failed job answers with the HTTP status in its error.code. The codes are listed on GET /jobs/jobid.

{ // 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" \
       -H "Content-Type: application/json" \
       -H "Authorization: Bearer …" \
       -d '{
         "prompt": "A paper boat drifting down a rain gutter, close up",
         "duration": 6,
         "resolution": "1080p",
         "async": true
       }'
    
  • const token = "API token";
    const apiUrl = "https://api.useapi.net/v1/google-vids";
    const headers = { "Content-Type": "application/json", "Authorization": `Bearer ${token}` };
    
    const response = await fetch(`${apiUrl}/videos`, {
      method: "POST",
      headers,
      body: JSON.stringify({
        prompt: "A paper boat drifting down a rain gutter, close up",
        duration: 6,
        resolution: "1080p",
        async: true
      })
    });
    let job = await response.json();
    console.log("submitted", response.status, job);
    
    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.status);
    }
    if (job.status === "completed") {
      const video = await fetch(`${apiUrl}/media/${encodeURIComponent(job.result.mediaId)}`, { headers });
      console.log("video bytes", (await video.arrayBuffer()).byteLength);
    } else
      console.log("error", job.error);
    
  • import time
    import requests
    from urllib.parse import quote
    
    token = "API token"
    apiUrl = "https://api.useapi.net/v1/google-vids"
    headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
    
    data = {
        "prompt": "A paper boat drifting down a rain gutter, close up",
        "duration": 6,
        "resolution": "1080p",
        "async": True
    }
    response = requests.post(f"{apiUrl}/videos", headers=headers, json=data)
    job = response.json()
    print("submitted", response.status_code, job)
    
    while job.get("status") == "processing":
        time.sleep(10)
        job = requests.get(f"{apiUrl}/jobs/{quote(job['jobid'], safe='')}", headers=headers).json()
        print(job["status"])
    
    if job.get("status") == "completed":
        video = requests.get(f"{apiUrl}/media/{quote(job['result']['mediaId'], safe='')}", headers=headers)
        with open("video.mp4", "wb") as f:
            f.write(video.content)
    else:
        print("error", job.get("error"))
    
Try It