How to Make AI Influencer Videos with Avatars via the Google Vids API
12 min read • October 7, 2026
Table of contents
- Introduction
- What it cost
- Before you start
- How it was built
- Examples
- What we learned
- Frequently asked questions
- Conclusion
Introduction
The useapi.net Google Vids API makes talking AI influencer videos from avatars: save a picture and one of 30 Gemini voices once with POST /avatars, then pass the avatar’s id to any Gemini Omni 1.1 Flash clip, where it speaks the lines you write in the prompt.
The API runs on your own Google Vids account. This walkthrough makes two influencers and launches a sneaker with them. Mia’s avatar comes from a portrait made with the API, and Leo’s is drawn by Google from a written description. They present the AeroLoop, a made-up sneaker, on a theater stage in one 10-second clip with both avatars and a product image, and two extends take it to 30 seconds. An edit moves the scene to a rooftop, and two more clips show what the same calls make without avatars. Every request below is the one that made the media on this page, with the ids replaced and async: true added by the run helper.
The finished 30-second clip at 720p: one 10-second generation and two 10-second extends (steps 6 to 8).
What it cost
useapi.net is an API, not a video service of its own. Every call runs on a Google account you connect through Setup Google Vids, on a paid Google AI plan, and uses that account’s monthly Vids allowance: seconds of video and a count of images. Google AI Ultra $199 gets 10,000 seconds of video and 1,000 images a month. You pay useapi.net a flat $15/month for every API we offer, with no per-clip charge. See Plans and monthly allowances for the other plans.
What each kept result used, taken from the change in result.quota, and how long each job took (result.elapsedMs):
| Step | Endpoint | Allowance used | Time |
|---|---|---|---|
| Mia’s portrait | POST /images | 1 image | 9 s |
| The AeroLoop product shot | POST /images | 1 image | 9 s |
| Mia’s avatar, from her portrait | POST /avatars | nothing | about 2 s |
| Leo’s avatar, from a description | POST /avatars | 1 image | about 20 s |
| The 10-second launch clip | POST /videos | 10 s | 38 s |
| Extend to 20 seconds | POST /videos/extend | 10 s | 70 s |
| Extend to 30 seconds | POST /videos/extend | 10 s | 85 s |
| Upscale the 20 seconds to 1080p | POST /videos/upscale | 20 s | 62 s |
| Rooftop edit | POST /videos/edit | 10 s | 64 s |
| Edit of the 30-second clip | POST /videos/edit | 10 s | 87 s |
| Product hero clip | POST /videos | 10 s | 62 s |
| The pigeon | POST /videos | 5 s | 54 s |
| Total | 85 s of video, 3 images |
An extend uses only the seconds it adds, not the length of the clip. An upscale uses the clip’s full length again, and an edit uses the length of the clip it returns. 720p and 1080p cost the same when you generate. A clip you generate again costs its seconds again.
What a 10-second Omni clip costs
| Where | One 10-second Omni 1.1 Flash clip | 10-second clips a month |
|---|---|---|
| Google’s metered Gemini API | about $1.01 at 720p | as many as you pay for |
| Google Flow API on Google AI Ultra ($199) | 15 credits, about $0.12 | about 1,666 |
| Google Vids API on Google AI Ultra ($199) | 10 seconds of allowance, about $0.20 | about 1,000, shared by the family plan |
| Flow and Vids on the same Ultra account | about $0.07 | about 2,666 |
The Gemini API bills Omni 1.1 Flash at 5,792 tokens per second of 720p video, at $17.50 per million tokens. At that rate, 2,666 ten-second clips would cost about $2,700 a month, against one Ultra plan and the $15 useapi.net subscription. Prices as of October 2026.
Before you start
You need a useapi.net API token and a Google account on a paid Google AI plan connected through Setup Google Vids. A free Google account gets no Vids allowance, so the API refuses it. Every call is also in the Postman collection.
The steps use curl, jq and these variables and helpers:
Expand the variables and helpers
export USEAPI_TOKEN="user:12345-..."
export EMAIL="[email protected]"
API=https://api.useapi.net/v1/google-vids
AUTH="Authorization: Bearer $USEAPI_TOKEN"
enc() { jq -rn --arg v "$1" '$v|@uri'; }
post() { curl -sS -X POST "$API/$1" -H "$AUTH" -H "Content-Type: application/json" -d "$2"; }
get() { curl -sS "$API/$1" -H "$AUTH"; }
download() { curl -sSf "$API/media/$(enc "$1")" -H "$AUTH" -o "$2"; }
# run a video job async: submit it, poll GET /jobs/{jobid} every 10 s, save the final job record to $1
run() {
local job id
job=$(post "$2" "$(jq '. + {async: true}' <<< "$3")")
id=$(jq -r .jobid <<< "$job")
while [ "$(jq -r .status <<< "$job")" = processing ]; do
sleep 10
job=$(get "jobs/$(enc "$id")")
done
echo "$job" > "$1"
jq '{status, error, duration: .result.duration, resolution: .result.resolution, videoLeft: .result.quota.video.left}' "$1"
}
Every generation runs as a job. Without async, a POST waits up to about 100 seconds and returns the finished job record, or 202 with the job still running. A new 10-second clip took 38 to 62 seconds here, and extends, edits and the upscale took 62 to 87. Some take longer than the sync wait, so run submits video jobs with async: true and polls GET /jobs/jobid. If run prints any status other than completed, stop there and read its error: the lines after it expect a finished clip.
To be called back instead of polling, add replyUrl: "https://example.com/hooks/vids" (and optionally replyRef: "launch-push-in") to any body below. The API then sends one POST of the final job record, as JSON, to that URL when the job completes or fails.
Ids contain : and @, so enc URL-encodes them whenever they go into a URL path. Every input of one video must come from the same Google account. The images and the upload pin email, Mia’s avatar inherits the portrait’s account, and a video job runs on the account that holds its inputs. The pigeon clip has no inputs, so add email to it if you connect more than one account.
How it was built
1. Mia’s portrait
POST /images makes one JPEG per call, here in portrait (9:16, 768×1376) with the photography style.
curl — POST /images
post images "$(jq -n --arg email "$EMAIL" '{email: $email, aspectRatio: "9:16", style: "photography",
prompt: "Full-length studio portrait of a confident 24-year-old female fashion influencer, platinum bob, oversized cream hoodie, wide-leg cargo pants, gold hoops, plain light-grey backdrop, facing camera"}')" > mia-portrait.json
MIA_IMAGE=$(jq -r .result.mediaId mia-portrait.json)
download "$MIA_IMAGE" mia-portrait.jpg

Generated images stay available for a few hours only. Make the avatar from it right away, and see the product hero clip for using an image later.
2. The AeroLoop product shot
The same call in landscape (16:9, 1376×768) gives the product reference the clips use.
curl — POST /images
post images "$(jq -n --arg email "$EMAIL" '{email: $email, aspectRatio: "16:9", style: "photography",
prompt: "The AeroLoop sneaker: chunky white-and-electric-blue running shoe with a translucent air sole, on a concrete pedestal, dramatic side light, dark background"}')" > aeroloop.json
SHOE_IMAGE=$(jq -r .result.mediaId aeroloop.json)
download "$SHOE_IMAGE" aeroloop.jpg

3. Pick the voices
GET /voices lists the 30 voices with Google’s English sample of each. It is public and needs no token. Mia speaks with Aoede (Vids calls it Tova, style “Breezy”) and Leo with Puck (Neo, “Upbeat”). POST /avatars takes either name.
curl -sS "$API/voices" | jq -r '.voices[] | "\(.name)\t\(.voice)\t\(.style)\t\(.preview)"'
4. Mia’s avatar from her portrait
POST /avatars with image set to the portrait’s mediaId saves Mia as an avatar on the same account in about 2 seconds. It costs nothing beyond the image.
curl — POST /avatars and the response
post avatars "$(jq -n --arg img "$MIA_IMAGE" '{name: "Mia", voice: "Aoede", image: $img}')" > mia-avatar.json
MIA_AVATAR_ID=$(jq -r .avatarId mia-avatar.json)
{
"avatarId": "user:[email protected]:eyJnIjoiaDM5…",
"name": "Mia",
"voice": "Tova",
"voiceId": "Aoede",
"voiceStyle": "Breezy",
"voicePreview": "https://ssl.gstatic.com/docs/videos/audio/voice_samples/gemini-v4s-tts/1.1/en/Aoede.wav",
"email": "[email protected]",
"source": "image"
}
An avatar cannot be made from your own photo. A real person’s avatar is Google’s likeness feature, which needs a live selfie video and a phone check in the browser, so POST /avatars refuses an uploaded photo with 400. Use an image made with the API, as here, or a description, as in the next step.
5. Leo’s avatar from a description
With appearance instead of image, Google draws the person. outfit and shot are optional, and so are expression and details. Drawing the picture uses 1 image of the allowance, and the response carries it as previewMediaId. In our runs a description avatar came out 1280×720 (16:9), whatever shot we picked.
curl — POST /avatars and the response
post avatars "$(jq -n --arg email "$EMAIL" '{email: $email, name: "Leo", voice: "Puck", shot: "upper-body",
appearance: "a 26-year-old white European male streetwear influencer with short textured hair with a fade and a confident half-smile",
outfit: "black puffer vest over a white tee, silver chain"}')" > leo-avatar.json
LEO_AVATAR_ID=$(jq -r .avatarId leo-avatar.json)
download "$(jq -r .previewMediaId leo-avatar.json)" leo-avatar.jpg
{
"avatarId": "user:[email protected]:eyJnIjoiaDM5…",
"name": "Leo",
"voice": "Neo",
"voiceId": "Puck",
"voiceStyle": "Upbeat",
"voicePreview": "https://ssl.gstatic.com/docs/videos/audio/voice_samples/gemini-v4s-tts/1.1/en/Puck.wav",
"email": "[email protected]",
"source": "generated",
"previewMediaId": "user:[email protected]:eyJ1IjoiaHR0…",
"look": {
"appearance": "a 26-year-old white European male streetwear influencer with short textured hair with a fade and a confident half-smile",
"outfit": "black puffer vest over a white tee, silver chain",
"shot": "upper-body"
}
}
![]()
Both avatars stay in the account’s Vids document until you delete them with DELETE /avatars/avatarId. GET /avatars lists them, with their ids, whenever you need them again:
get "avatars?email=$(enc "$EMAIL")" | jq -r '.avatars[] | "\(.name)\t\(.voiceId)\t\(.avatarId)"'
6. The launch: two avatars and the product in one clip
POST /videos takes up to 3 references in all, avatars (avatar_1..avatar_3) and images (referenceImage_1..referenceImage_3) together. In the prompt, @avatar_1, @avatar_2 and @referenceImage_1 stand for them, and each avatar speaks its quoted line in its own voice. The product image’s mediaId goes in directly: the API fetches the image and uploads it on that account.
curl — POST /videos and the job record
PROMPT=$(cat <<'EOF'
@avatar_1, holding @referenceImage_1 in both hands at chest height, and @avatar_2 stand side by side on a well-lit stage in a small, intimate theater at a sneaker launch, shown full length from head to toe in a wide, steady shot. Behind them a large LED backdrop glows with soft white-and-blue light that moves in slow, gentle waves, with the words "WEAR AEROLOOP" in large, clean, bold white letters across the middle of the screen. The two of them stand on the lower part of the stage, so the words "WEAR AEROLOOP" stay fully readable above their heads. Bright, even stage lighting on both of them. The audience sits in the dark in front of the stage, only the tops of a few heads barely visible at the bottom of the frame. @avatar_1 keeps holding @referenceImage_1 the whole time and says: "Say hi to the AeroLoop!" @avatar_2 turns to her, smiles and says: "Lighter than anything we've ever worn." Simple, uncluttered composition, the two of them clearly the focus.
EOF
)
run launch-10s.json videos "$(jq -n --arg p "$PROMPT" --arg mia "$MIA_AVATAR_ID" --arg leo "$LEO_AVATAR_ID" --arg shoe "$SHOE_IMAGE" \
'{prompt: $p, avatar_1: $mia, avatar_2: $leo, referenceImage_1: $shoe, duration: 10, resolution: "720p", aspectRatio: "landscape"}')"
LAUNCH_10S=$(jq -r .result.mediaId launch-10s.json)
download "$LAUNCH_10S" launch-10s.mp4
The job record, with the ids and the prompt shortened. It was made with a sync call, so its request has no async:
{
"jobid": "user:[email protected]:7de4eaa5-64ed-4f24-b633-1eaab4564426",
"type": "video",
"mode": "ingredients",
"email": "[email protected]",
"status": "completed",
"created": "2026-10-08T00:29:18.374Z",
"request": {
"prompt": "@avatar_1, holding @referenceImage_1 in both hands at chest height, and @avatar_2 stand side by side …",
"avatar_1": "user:[email protected]:eyJnIjoiaDM5…",
"avatar_2": "user:[email protected]:eyJnIjoiaDM5…",
"referenceImage_1": "user:[email protected]:eyJ1IjoiaHR0…",
"duration": 10,
"resolution": "720p",
"aspectRatio": "landscape"
},
"updated": "2026-10-08T00:29:56.162Z",
"completed": "2026-10-08T00:29:56.162Z",
"result": {
"mediaId": "user:[email protected]:eyJ1IjoiaHR0…",
"width": 1280,
"height": 720,
"duration": 10,
"resolution": "720p",
"aspectRatio": "landscape",
"model": "/flix/generate_videos_omni_r2v_psq/v1",
"quota": {
"video": {
"limit": 10000,
"left": 9164,
"resetAt": "2026-11-01T07:00:00.000Z"
}
},
"elapsedMs": 37788
}
}
10 seconds, 720p, 38 seconds to generate.
7. Extend to 20 seconds
POST /videos/extend adds 3 to 10 seconds and returns the whole clip with a new mediaId. An extend takes only the clip and a prompt, not the avatars, so the prompt describes the new action and names people by what they wear (“the man in the black puffer vest”). A fan in a red hoodie storms the stage.
curl — POST /videos/extend
PROMPT=$(cat <<'EOF'
A young man in a red hoodie climbs onto the stage from the front right edge and plants himself to the right of the man in the black puffer vest, an arm's length away, so all three stand apart in a row with nobody overlapping. Visibly overexcited, he waves a thick stack of banknotes in his right hand above his head and screams: "Take my money!" The man in the black puffer vest laughs, raises both palms toward him and says: "Whoa, whoa, buddy, be patient!" The blonde woman in the cream hoodie laughs, holding the sneaker. Only these three people are on stage. Same stage, same LED screen with WEAR AEROLOOP, same wide steady shot.
EOF
)
run launch-20s.json videos/extend "$(jq -n --arg id "$LAUNCH_10S" --arg p "$PROMPT" '{mediaId: $id, prompt: $p, duration: 10}')"
LAUNCH_20S=$(jq -r .result.mediaId launch-20s.json)
download "$LAUNCH_20S" launch-20s.mp4
20 seconds, 720p, 70 seconds to generate. It used 10 seconds of allowance, the seconds it added.
8. Extend to 30 seconds
The second extend continues the 20-second clip.
curl — POST /videos/extend
PROMPT=$(cat <<'EOF'
The blonde woman in the cream hoodie turns to the camera with a big smile and says: "AeroLoop drops Friday. Link in bio!" Right after that, the young man in the red hoodie throws ALL of his banknotes out over the audience in one big toss, keeping none, and the bills flutter down over their heads. From then on his hands are completely empty: he holds no money at all. Then all three cheer together, the man in the black puffer vest and the young man in the red hoodie throwing their empty fists in the air, as blue and white confetti rains down over the stage. Only these three people are on stage. Same stage, same LED screen with WEAR AEROLOOP, same wide steady shot.
EOF
)
run launch-30s.json videos/extend "$(jq -n --arg id "$LAUNCH_20S" --arg p "$PROMPT" '{mediaId: $id, prompt: $p, duration: 10}')"
LAUNCH_30S=$(jq -r .result.mediaId launch-30s.json)
download "$LAUNCH_30S" launch-30s.mp4
The result is the finished clip: 30 seconds, 720p, 85 seconds to generate, and again 10 seconds of allowance. You can keep going: chains of extends work up to 40 seconds, for example four 10-second steps. Past that, Google keeps only about the first 31 seconds of the source and adds the new seconds after them, so the longest clip is about 41 seconds.
9. Upscale to 1080p
POST /videos/upscale turns a 720p clip into 1080p and keeps its length. It works on clips of up to 20 seconds, so we upscaled the 20-second clip from step 7, not the 30-second one. It used the clip’s 20 seconds again and took 62 seconds.
curl — POST /videos/upscale
run launch-20s-1080p.json videos/upscale "$(jq -n --arg id "$LAUNCH_20S" '{mediaId: $id}')"
download "$(jq -r .result.mediaId launch-20s-1080p.json)" launch-20s-1080p.mp4
The 20-second clip, upscaled to 1920×1080.
Why not the 30-second clip: Google returned it at 1920×1080, but with its frames out of order, three times out of three. The confetti ending flickered in before the fan arrived. The 20-second clip came back in order, and the Vids editor itself stops at 20 seconds, so the API refuses upscales of clips longer than 20 seconds with 400, before anything is charged.
So for a 1080p clip:
- Make, extend and edit at
720p. - Upscale the clip once it is finished, while it is 20 seconds or shorter.
- For a longer clip, stay at
720p, setresolution: "1080p"on the last extend only, or extend at1080pthroughout and accept softer faces.
Examples
More from the same account and inputs: an edit, a product clip from a start image, and a text-only clip.
Move the launch to a rooftop
POST /videos/edit changes a generated clip by prompt and returns a new clip. The source goes in video. This edit of the 10-second launch clip used 10 seconds and took 64 seconds.
curl — POST /videos/edit
run launch-rooftop.json videos/edit "$(jq -n --arg v "$LAUNCH_10S" \
'{video: $v, prompt: "Move the whole scene to a rooftop at sunset, with the city skyline behind them. Same two people, same action, same words."}')"
download "$(jq -r .result.mediaId launch-rooftop.json)" launch-rooftop.mp4
An edit works on up to 10 seconds. The same edit of the 30-second clip, with “Same three people”, came back 10 seconds long: the first 10 seconds, moved to the rooftop. It used 10 seconds of allowance. To get a longer edited clip, edit the 10-second clip and extend the result, since an extend accepts an edited clip.
The edit of the 30-second clip: 10 seconds back.
A product hero clip from a start image
With startImage the clip opens on that image. A generated image’s link expires within hours, so we uploaded the saved aeroloop.jpg with POST /assets and passed its assetId. A single clip has no extends, so it was made at 1080p directly.
curl — POST /assets and POST /videos
SHOE_ASSET=$(curl -sS -X POST "$API/assets?email=$(enc "$EMAIL")" -H "$AUTH" -H "Content-Type: image/jpeg" \
--data-binary @aeroloop.jpg | jq -r .assetId)
run hero.json videos "$(jq -n --arg img "$SHOE_ASSET" '{startImage: $img, duration: 10, resolution: "1080p", aspectRatio: "landscape",
prompt: "Slow 360° orbit around the sneaker, blue light rippling through the translucent sole, dust sparkling in the air, cinematic."}')"
download "$(jq -r .result.mediaId hero.json)" hero.mp4
10 seconds, 1080p.
A pigeon in AeroLoops
A text-only clip, in portrait at 1080p (1080×1920). The 5 seconds took 54 seconds to generate and used 5 seconds of allowance.
curl — POST /videos
run pigeon.json videos "$(jq -n '{duration: 5, resolution: "1080p", aspectRatio: "portrait",
prompt: "A pigeon in tiny white-and-electric-blue AeroLoop sneakers struts down a city sidewalk like a runway model, head bobbing to the beat. Pedestrians stop and film it with their phones. It pauses, does a slow-motion hair flip with its head feathers, and struts on. Funny, sunny, shot on a phone at pigeon height."}')"
download "$(jq -r .result.mediaId pigeon.json)" pigeon.mp4
What we learned
- A
1080pclip behaves like an upscaled720pone. Build a chain of extends at720p: at1080pthe faces got softer and more waxy at every step. - Upscale only clips of up to 20 seconds. Our 30-second upscale came back with its frames out of order every time.
- Generate a single clip at
1080pif you want1080pand won’t extend it. It costs the same as720p. - Write an extend prompt about the new action only. The extend gets the clip, not the avatars, so name people by what they wear.
- Put a prop in a person’s hands in the first sentence that mentions them. A verb like “lifts” made the sneaker appear from nowhere.
- Say what must not happen. The fan kept a stack of banknotes until the prompt said his hands were empty.
- Reuse avatars by
avatarId. Each avatar is a fixed picture and voice, kept until you delete it. - Make avatars and clips from a generated image within a few hours, or upload the saved file with POST /assets first.
- Edit clips of up to 10 seconds. A longer clip comes back as its first 10 seconds.
- Expect on-screen text to waver. The backdrop’s “WEAR AEROLOOP” is misspelled in some frames.
- Every MP4 has Google’s visible Gemini ✦ in the bottom-right corner and an invisible SynthID watermark (see Watermark).
Frequently asked questions
How do I make an AI influencer video through an API? Create an avatar with POST /avatars, from a description or from an image made with POST /images, and give it a voice from GET /voices. Then pass its avatarId as avatar_1 to POST /videos and write its lines in quotes in the prompt.
Can I make an avatar from my own photo? No. Google treats an avatar of a real person as its likeness feature, which needs a live selfie video and a phone check in the browser. The API refuses an uploaded photo with 400. Use a picture made with POST /images or describe the person with appearance.
How many avatars can one clip have? Up to 3 references in all, avatars and images together. This walkthrough used two avatars and one product image.
Does an avatar look and sound the same in every video? It uses the same picture and the same voice every time you pass its avatarId, since both are saved with the avatar in the account’s Vids document. The clip around it still varies from take to take. GET /avatars lists your avatars, including ones made in the Google Vids web app.
Can two avatars talk to each other in one clip? Yes. Pass avatar_1 and avatar_2 and give each a quoted line in the prompt, as in step 6.
Can I make a vertical 9:16 video for TikTok or Reels? Yes. Set aspectRatio: "portrait" on POST /videos for 720×1280, or 1080×1920 at 1080p, as in the pigeon clip. An extend can also turn a landscape clip into portrait.
How long can a video be? A generated clip is 3 to 10 seconds. Extends add 3 to 10 seconds each, up to about 41 seconds in all, and each one uses only the seconds it adds.
What does it cost? A flat $15/month to useapi.net, plus the Vids allowance of your own Google AI plan: Ultra $199 gets 10,000 seconds of video and 1,000 images a month. Everything kept on this page used 85 seconds and 3 images (see What it cost).
Why did my sync request return 202? The job was still running after about 100 seconds. It keeps running on our side. Fetch it with GET /jobs/jobid, or send long jobs with async: true.
Conclusion
Visit our Discord Server or
Telegram Channel for any support questions and concerns.