Make an avatar talk
November 4, 2026
Table of contents
Make a talking-head video of one avatar: the avatar faces the camera and says your script word for word, in its own voice, with matching lip movement. The video is an MP4 with sound, 1280×720 (landscape only, Google offers no portrait), and as long as the spoken script, up to 30 seconds.
How it differs from an avatar in POST /videos:
| POST /videos/avatar | POST /videos with avatar_1 | |
|---|---|---|
| What you get | A talking head reading your script | A scene with action, where the avatar is a character |
| Length | The length of the speech, up to 30 seconds | 3 to 10 seconds |
| What the avatar says | Exactly your script | The lines you write in the prompt |
| Format | 720p landscape | 720p or 1080p, landscape or portrait |
| Allowance | 1 AI avatar video (and 1 Gemini use for the speech) | The clip’s seconds of video |
Audio tags in square brackets, such as [whisper], [short pause] or [sarcasm], are performed, not read aloud. To hear how the avatar’s voice reads a script before you spend an avatar video, try it with POST /speech in the same voice.
The video runs on the account that holds the avatar. Its result is a video mediaId, which downloads with GET /media/mediaId and can be passed to POST /videos/extend, POST /videos/upscale and POST /videos/edit like any other clip.
Cost
- Each video uses 1 AI avatar video from the account’s monthly allowance, shown as
avatarVideoin GET /accounts/email. It does not use the account’s video seconds. - The speech is made first and uses 1 of the account’s monthly Gemini uses (
speech), like POST /speech. - Google checks the video while making it: for safety, and for a picture that looks like a prominent person. This check is stricter than the one for avatar pictures, so an avatar made from a photo of someone who is not famous can still be refused here. When Google refuses, the job fails with
422and Google still counts the video against the allowance. A script Google refuses before the video starts costs nothing. - A script that runs over 30 seconds when spoken fails with
400before the video starts. The speech use is spent.
Sync, async and webhooks
Google takes about 1 to 2.5 minutes to make a talking avatar video. A sync request waits about 100 seconds and then answers 202 with the job still running, so plan to fetch the result with GET /jobs/jobid or a replyUrl webhook, or pass async: true.
- 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
200with the finished job record, or the job’s error code. A video usually takes 20 to 100 seconds, a talking avatar video 1 to 2.5 minutes, an image about 10 and speech a few. If the job is still running after about 100 seconds, the POST answers202with the job as it stands: fetch the result with GET /jobs/jobid. Always checkstatus: onlycompletedhas aresult. - Async: pass
async: true. The POST answers202at once with the job instatus: "processing". Poll GET /jobs/jobidor wait for the webhook. replyUrlreceives onePOSTof 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
maxJobsjobs at a time (default3, range1to10, 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/avatar
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
{
"avatar": "user:[email protected]:eyJnIjoiaDMy…",
"script": "Hi there! Thanks for joining us today. Let me show you what we built this week.",
"async": true,
"replyUrl": "https://your-domain.com/webhook",
"replyRef": "weekly-update-1"
}
avataris required, anavatarIdfrom POST /avatars or GET /avatars. The video runs on that avatar’s account.scriptis required, what the avatar says, written as it should be spoken. Audio tags in square brackets are performed, not read. Spoken, it must fit in 30 seconds: depending on the voice that is about 250 to 400 characters, and[slow pace]or pauses make it longer (370 characters with[slow pace]ran 33 seconds in a calm voice).
Length:1to400characters.asyncis optional,trueto answer202at once and run the job in the background. Default:false.replyUrlis optional, a publichttp(s)URL that receives onePOSTof 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/jobidresponse.
Maximum length:1024characters.replyRefis optional, your own reference echoed back in the job record.
Maximum length:1024characters.
Any other parameter returns 400 Parameter <name> not supported.
Responses
-
200 OK — the video is ready. In sync mode this is rare, since most videos take longer than the sync wait. GET /jobs/
jobidreturns the same record.{ "jobid": "j1104215335919482713a-u12345-email:[email protected]:google-vids", "type": "avatarVideo", "email": "[email protected]", "status": "completed", "created": "2026-11-04T21:53:35.919Z", "request": { "avatar": "user:[email protected]:eyJnIjoiaDMy…", "script": "Hi there! Thanks for joining us today. Let me show you what we built this week.", "async": true }, "updated": "2026-11-04T21:54:34.055Z", "completed": "2026-11-04T21:54:34.055Z", "result": { "mediaId": "user:[email protected]:eyJ1IjoiaHR0…", "width": 1280, "height": 720, "duration": 4, "resolution": "720p", "aspectRatio": "landscape", "model": "/flix/generate_avatar_video/v2_conditional", "speechDuration": 4.28, "quota": { "avatarVideo": { "limit": 10, "left": 9, "resetAt": "2026-12-01T08:00:00.000Z" } }, "elapsedMs": 58136 } } -
202 Accepted — the job is running: with
async: trueat once, in sync mode after about 100 seconds of waiting. Fetch the result with GET /jobs/jobid(or wait for the webhook).{ "jobid": "j1104215335919482713a-u12345-email:[email protected]:google-vids", "type": "avatarVideo", "email": "[email protected]", "status": "processing", "created": "2026-11-04T21:53:35.919Z", "request": { "avatar": "user:[email protected]:eyJnIjoiaDMy…", "script": "Hi there! Thanks for joining us today. Let me show you what we built this week.", "async": true } } -
400 Bad Request — a parameter is missing or invalid, the avatar uses an older voice, the avatar’s account has no AI avatar videos on its plan (a monthly limit of 0), or the spoken script is longer than 30 seconds (the failed job record).
{ "error": "Parameter script length exceeds 400 characters", "code": 400 }An avatar made in the Vids web app with one of Google’s older narrator voices cannot talk through the API. Make a new avatar with POST /avatars, for example from this one with
avatar.{ "error": "This avatar uses an older Vids narrator voice that the API cannot send. Make a new avatar with POST /avatars", "code": 400 }A script that runs past 30 seconds when spoken:
{ "error": { "code": 400, "message": "The script runs 34.2 seconds when spoken in this voice. An avatar video takes up to 30 seconds of speech (Google's limit): shorten the script" } }The body is the failed job record, shortened here to its
error. -
Invalid API token.
{ "error": "useapi.net ⁝ Unauthorized", "code": 401 } -
403 Forbidden — the
avatarIdwas issued to a different API token, Google answered that the account’s plan has no AI avatar videos, or the account has Google Workspace smart features turned off (the failed job record,error.code: 403).{ "error": "avatar id does not belong to this API token", "code": 403 } -
404 Not Found — the account that holds the avatar is not connected.
{ "error": "Account [email protected] (from the avatar id) is not configured", "code": 404 } -
422 Unprocessable Content — Google refused the script, or refused the video while making it (its safety check, or an avatar picture that looks like a prominent person). A refused script costs nothing. A refused video still counts against the account’s AI avatar videos. Change the script or use another avatar and try again.
{ "error": { "code": 422, "message": "Google's safety check refused the finished avatar video (FINISH_REASON_GENERATED_VIDEO_SAFETY). Google still counts it against the account's monthly AI avatar allowance. Change the script or the avatar and try again" } }An avatar picture Google takes for a prominent person:
{ "error": { "code": 422, "message": "Google refused to animate this avatar: its picture looks like a prominent person (FINISH_REASON_INPUT_IMAGE_CELEBRITY). Google still counts it against the account's monthly AI avatar allowance. Use an avatar of someone else" } }The body is the failed job record, shortened here to its
error. -
429 Too Many Requests — the account is running
maxJobsjobs, its AI avatar videos or Gemini uses for this month are used up, or Google is limiting the account for a while.{ "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 AI avatar video allowance is used up, the API keeps Google’s reset time on the account (
exhaustedUntilin GET /accounts) and gives it no AI avatar video jobs until then. A request that must run on that account is answered at once with429, with no job created and no call to Google.retryAtis when to try again. If the account’s plan changed, GET /accounts/emailreads its allowance again and lets it take jobs right away.{ "error": "Account [email protected] has used up this month's Google Vids AI avatar 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.code429,error.retryAtset to the reset time, anderror.quotawith what was left (limit,left,resetAt). An account with some allowance left still takes smaller jobs. -
502 Bad Gateway — Google could not make the video and gave another reason, or answered in a way the API could not read. The body is the failed job record, and
error.messagehas Google’s reason. -
503 Service Unavailable — Google answered with a server error or could not be reached, or the async job could not be queued. Retry shortly. The body is the failed job record.
-
504 Gateway Timeout — Google did not finish the video within the job’s time budget. It may still count against the account’s AI avatar videos: run it again.
{ "error": { "code": 504, "message": "Google did not finish the avatar video within the job's time budget. It may still count against the account's AI avatar allowance: 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, type: "avatarVideo". GET /jobs/jobid and the replyUrl webhook return the same shape. The result’s duration is the video’s length in whole seconds and speechDuration the exact length of the spoken script. The codes are listed on GET /jobs/jobid.
{ // TypeScript, all fields are optional
jobid: string // j<time><random><v|i|s|a>-u<id>-email:<email>-bot:google-vids
type: 'video' | 'image' | 'speech' | 'avatarVideo'
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, images, Gemini uses or AI avatar videos)
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. Speech: a WAV
width?: number // video and image
height?: number // video and image
duration?: number // video: length of the whole clip in whole seconds. Speech: length of the audio in seconds
resolution?: '720p' | '1080p' // video
aspectRatio?: 'landscape' | 'portrait' // video
speechDuration?: number // avatarVideo: length of the spoken script in seconds
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 (absent when Google sends no counter)
video?: { limit: number, left: number, resetAt: string } // seconds, video jobs
image?: { limit: number, left: number, resetAt: string } // images, image jobs
speech?: { limit: number, left: number, resetAt: string } // Gemini uses, speech jobs; absent on plans Google does not meter (Ultra)
avatarVideo?: { limit: number, left: number, resetAt: string } // AI avatar videos, avatarVideo jobs
}
elapsedMs: number // how long Google took
}
}
Examples
-
curl -X POST "https://api.useapi.net/v1/google-vids/videos/avatar" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer …" \ -d '{ "avatar": "user:[email protected]:eyJnIjoiaDMy…", "script": "Hi there! Thanks for joining us today. Let me show you what we built this week.", "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/avatar`, { method: "POST", headers, body: JSON.stringify({ avatar: "user:[email protected]:eyJnIjoiaDMy…", script: "Hi there! Thanks for joining us today. Let me show you what we built this week.", async: true }) }); let job = await response.json(); console.log("submitted", response.status, job); while (job.status === "processing") { await new Promise(r => setTimeout(r, 15000)); 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 = { "avatar": "user:[email protected]:eyJnIjoiaDMy…", "script": "Hi there! Thanks for joining us today. Let me show you what we built this week.", "async": True } job = requests.post(f"{apiUrl}/videos/avatar", headers=headers, json=data).json() print("submitted", job) while job.get("status") == "processing": time.sleep(15) 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("avatar.mp4", "wb") as f: f.write(video.content) else: print("error", job.get("error"))
Try It
A sync request waits about 100 seconds and usually answers 202: then paste the jobid into GET /jobs/jobid a minute later to see the video.