How to Generate AI Video with Veo 3.1 via the Google Flow API
6 min read β’ June 15, 2026
Table of contents
- Introduction
- Supported models
- Pricing
- Generate a video in two API calls
- Image-to-video and reference-to-video
- Batch-generate with a script
- Examples
- Frequently asked questions
- Conclusion
Introduction
Veo 3.1, Googleβs flagship video model, runs from a single curl through useapi.net. You drive your own Google Flow account β no Google Cloud project, no enterprise approval, and up to several times lower cost per generation than the official metered API.
The official Gemini API and Vertex meter Veo per second of output. This third-party Google Flow API runs on a flat subscription instead β you spend your normal Flow credits plus one monthly fee for API access, far cheaper per clip. Below: the models you get, the price gap versus the official API, then the two-call workflow and a runnable batch script.
Supported models
Pick a model per request with the model field (default veo-3.1-fast). Credit costs come from Googleβs official Flow credits table:
| Model id | Tier / speed | Durations | Credits per generation |
|---|---|---|---|
veo-3.1-lite | Cheapest Veo | 4s, 6s, 8s | 10 (Non-Ultra) Β· 5 (Ultra) |
veo-3.1-lite-low-priority | Lite, lower priority | 4s, 6s, 8s | 0 β Ultra $199 only |
veo-3.1-fast (default) | Fast Veo | 4s, 6s, 8s | 20 (Non-Ultra) Β· 10 (Ultra) |
veo-3.1-quality | Highest-quality Veo | 8s only | 100 |
omni-flash | Gemini Omni Flash, audio-native | 4s, 6s, 8s, 10s | 15 / 20 / 25 / 30 (by length) |
Notes from the overview: 4s/6s are Ultra-only on Veo, veo-3.1-quality is 8s only, veo-3.1-lite-low-priority costs 0 credits but is available only to Google AI Ultra $199 subscribers, and 10s clips are omni-flash only. Google Flow can also generate stills with Imagen 4 and the Nano Banana models via the separate POST /images endpoint.
Pricing
You spend your Google AI planβs Flow credits as usual, plus a flat $15/month to useapi.net for API access to every supported service β no per-call surcharge.
Third-party Google Flow API by useapi.net vs. the official Gemini API β drive your own Google Flow subscription instead of metered, per-call API billing:
| Model | Official Gemini API | useapi.net (Flow Pro) | useapi.net (Flow Ultra) |
|---|---|---|---|
| Veo 3.1 Fast β 8s clip | $0.80 | ~$0.40 | ~$0.10 |
| Veo 3.1 Quality β 8s clip | $3.20 | ~$2.00 | ~$1.00 |
| Veo 3.1 Lite β 8s clip | $0.40 | ~$0.20 | ~$0.05 |
| Veo 3.1 Lite, lower priority | β | β | $0 (Ultra $199) |
| Gemini Omni Flash β 8s clip | β (Flow only) | ~$0.50 | ~$0.25 |
| Nano Banana Pro β per image | $0.134 | included | included |
| Nano Banana β per image | $0.039 | included | included |
| Imagen 4 β per image | metered | included | included |
Expected daily output β Ultra ($199/mo) plan. Daily averages observed on top real accounts. These are expected, not guaranteed: Google governs the underlying Flow allowances and they vary with demand.
| Free workload (no Flow credits spent) | Avg generations / day* |
|---|---|
| Veo 3.1 Lite β lower priority video | ~1,000 |
| Images β Nano Banana, Nano Banana Pro, Imagen 4 | up to ~500 |
*Averages from the busiest real accounts β not guarantees. They are set by Googleβs own allowances and fluctuate with demand. Applies only to the free lower-priority video queue and image generation. Credit-metered models (Veo 3.1 Fast / Quality and Gemini Omni Flash) are not shown here β their volume is bounded by your planβs monthly Flow credits (see the credit table), not a free allowance.
Veo is billed per second on the Gemini API (an 8-second 720p clip shown) and images are priced per image. Through useapi.net you spend your own Flow plan credits and pay a flat $15/month β image generation is included on any Google AI plan, and there is no Google Cloud project, API key, or per-call metering.
Full per-tier credit allowances and the per-model cost breakdown live on the Google Flow overview. See the setup guide to connect your account.
Generate a video in two API calls
You need a useapi.net API token and a connected Google Flow account. Generation is asynchronous: pass async: true and the create call returns a jobid immediately, then you poll until the video is ready.
- Submit the job β
POST https://api.useapi.net/v1/google-flow/videos:
curl -X POST "https://api.useapi.net/v1/google-flow/videos" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A serene mountain landscape at sunset, camera slowly panning right",
"model": "veo-3.1-fast",
"aspectRatio": "landscape",
"duration": 8,
"async": true
}'
In async mode the response returns immediately with 201 Created, a lowercase jobid, and status: "created":
{
"jobid": "j1731859234567v-u12345-email:jo***@gmail.com-bot:google-flow",
"type": "video",
"status": "created",
"created": "2026-06-15T12:34:56.789Z",
"request": {
"async": true,
"prompt": "A serene mountain landscape at sunset, camera slowly panning right",
"model": "veo-3.1-fast",
"aspectRatio": "landscape",
"duration": 8
}
}
- Poll for the result β
GET https://api.useapi.net/v1/google-flow/jobs/{jobId}:
curl "https://api.useapi.net/v1/google-flow/jobs/{jobId}" \
-H "Authorization: Bearer $USEAPI_TOKEN"
status moves through created β started β completed. When it reaches completed, the finished MP4 is in response.media[].videoUrl (a single prompt with count > 1 returns one entry per clip):
{
"jobid": "j1731859234567v-...",
"type": "video",
"status": "completed",
"response": {
"media": [
{
"mediaGenerationId": "user:12345-email:6a6f...-video:a1d95d21-...",
"videoUrl": "https://flow-content.google/video/a1d95d21-...?Expires=...",
"thumbnailUrl": "https://flow-content.google/image/a1d95d21-...?Expires=..."
}
],
"remainingCredits": 18760
}
}
Veo 3.1 typically finishes in 60β180 seconds depending on model and mode. The signed videoUrl is valid for about 24 hours, so download promptly. Prefer not to poll? Pass a replyUrl in the create body to receive a webhook callback when the job completes.
Image-to-video and reference-to-video
Veo takes image inputs two different ways. Upload each image first with POST /assets/email (raw bytes, an image Content-Type, PNG/JPEG/WebP up to 20 MB). The upload response nests the id at mediaGenerationId.mediaGenerationId β that nested string is what you pass back. When you supply any image input you can omit email: the request routes to the account where the image was uploaded.
First / last frame (I2V)
Your image becomes a literal frame of the clip. Pass it as startImage to start from that frame:
curl -X POST "https://api.useapi.net/v1/google-flow/videos" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Brave Captain Cat chasing away the pirates!",
"model": "veo-3.1-fast",
"aspectRatio": "portrait",
"startImage": "user:12345-email:6a6f...-image:ff9aa5cc-...",
"async": true
}'
Add endImage (a second uploaded reference id) to pin the last frame so Veo generates the transition between the two. endImage requires startImage β end-frame-only is not supported. On omni-flash, only startImage is accepted.
Reference images (R2V)
Your images guide the whole clip β subject, character, or style β rather than a specific frame. Pass referenceImage_1 through referenceImage_3 (Veo accepts up to 3, on 8-second clips only, and not on veo-3.1-quality). R2V cannot be combined with startImage/endImage:
curl -X POST "https://api.useapi.net/v1/google-flow/videos" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A lady says to the dog, \"Say meow meow meow.\" The dog looks at the lady with a surprised, frustrated look and woofs in a human voice, \"What the heck lady?!\"",
"model": "veo-3.1-fast",
"aspectRatio": "portrait",
"referenceImage_1": "user:12345-email:6a6f...-image:abcde-...",
"async": true
}'
Mention each reference inline with an @referenceImage_N marker in the prompt, add a referenceAudio_1 (a system voice name like Zephyr) for narration, or pass a reusable character as character_1 (it drives the same R2V mode and counts toward the 3-reference budget). Omni Flash takes references further β up to 7, plus video-to-video editing and synced voices β see the Omni Flash tutorial.
Batch-generate with a script
Finding the right shot takes many attempts, and running them by hand is tedious. The Node.js script below reads a list of prompts from prompts.json, uploads any start/end frame images, submits each job in async mode, then polls and downloads every finished MP4 β so you can queue a batch and come back to the winners.
You need Node.js v21 or newer. Put prompts.json and google-flow.mjs in the same folder and run node ./google-flow.mjs API_TOKEN EMAIL, where API_TOKEN is your useapi.net API token and EMAIL is your connected Google Flow account email. The script looks the account up by email automatically and checks its health field before submitting.
Prefer to clone and run it locally? The complete google-flow.mjs and prompts.json are on GitHub in useapi/google-flow-api.
Expand prompts.json
[
{
"prompt": "By default the veo-3.1-fast model with a landscape 8-second clip will be used."
},
{
"model": "veo-3.1-quality",
"aspectRatio": "landscape",
"duration": 8,
"count": 2,
"seed": 123456,
"prompt": "Highest-quality Veo 3.1, two variations. For all parameters see https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos"
},
{
"model": "veo-3.1-fast",
"aspectRatio": "portrait",
"duration": 8,
"startImage": "./first_image.jpeg",
"endImage": "./last_image.jpeg",
"prompt": "Image-to-video with a start frame (startImage) and an end frame (endImage). Veo only β omni-flash supports a start frame only."
},
{
"model": "omni-flash",
"duration": 10,
"prompt": "Gemini Omni Flash is audio-native and supports 10-second clips. Other models (veo-3.1-lite, veo-3.1-lite-low-priority, β¦) are selectable via the model field β see the POST /videos docs."
}
]
Expand google-flow.mjs script
/*
Script version 1.0, June 15, 2026
Script to batch-generate videos using prompts with the Google Flow API v1 by useapi.net π
Uses the POST /videos endpoint in async mode (default model: veo-3.1-fast) and polls GET /jobs/{jobId}.
For more details visit https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos
Installation Instructions:
==========================
You need Node.js v21 or newer installed to run this script. Download and install Node.js from:
- Windows, macOS, Linux: https://nodejs.org/
After installation, verify by running the following command in a terminal:
node -v
Running the Script:
===================
Usage: node google-flow.mjs <API_TOKEN> <EMAIL> [PROMPTS_FILE]
Replace API_TOKEN with your actual useapi.net API token, see https://useapi.net/docs/start-here/setup-useapi
Replace EMAIL with configured Google Flow email account, see https://useapi.net/docs/start-here/setup-google-flow
If optional PROMPTS_FILE not provided prompts.json will be used.
Example:
--------
node google-flow.mjs user:1234-abcdefhijklmnopqrstuv [email protected]
This command executes the script using API token user:1234-abcdefhijklmnopqrstuv with [email protected] Google Flow account email.
Changelog:
==========
- June 15, 2026: Initial release.
*/
import readline from 'node:readline';
import fs from 'fs/promises';
import path from 'path';
import { writeFile } from 'node:fs/promises';
import { Readable } from 'node:stream';
// Constants
const RESULTS_FILE = 'google-flow_results.txt';
const ERRORS_FILE = 'google-flow_errors.txt';
const DEFAULT_PROMPTS_FILE = 'prompts.json';
const DEFAULT_MODEL = 'veo-3.1-fast';
const SLEEP_429 = 30 * 1000; // in milliseconds
const SLEEP_POLL = 15 * 1000; // in milliseconds
const urlAccounts = 'https://api.useapi.net/v1/google-flow/accounts';
const urlVideos = 'https://api.useapi.net/v1/google-flow/videos';
const urlJobs = 'https://api.useapi.net/v1/google-flow/jobs/';
const urlUploadAsset = 'https://api.useapi.net/v1/google-flow/assets/';
// To upload .webp keep its .webp extension β Google Flow accepts png, jpeg and webp.
const supportedFileExtensions = ['png', 'jpeg', 'webp'];
// { filename: mediaGenerationId }
const uploadedFiles = {};
// Utility to sleep for given milliseconds
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Function to fetch configured Google Flow API accounts
async function fetchAccounts(apiToken) {
const response = await fetch(urlAccounts, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
});
if (!response.ok) {
console.error(`β Error fetching accounts (HTTP ${response.status}): ${response.statusText}`);
process.exit(1);
}
return response.json();
}
const elapsedTimeSec = (start) => (Date.now() - start) / 1000;
// Map a file extension to the Content-Type required by POST /assets
const contentTypeForExt = (ext) => ext === 'png' ? 'image/png' : ext === 'webp' ? 'image/webp' : 'image/jpeg';
async function uploadAsset(apiToken, email, filename) {
// Check if already uploaded
if (uploadedFiles.hasOwnProperty(filename))
return uploadedFiles[filename];
const startTime = Date.now();
console.log(`β¬οΈ Account ${email} uploading fileβ¦`, filename);
const body = new Blob([await fs.readFile(filename)]);
const fileExt = filename.split('.').pop();
const response = await fetch(`${urlUploadAsset}${encodeURIComponent(email)}`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`,
'Content-Type': contentTypeForExt(fileExt)
},
body
});
if (response.ok) {
const json = await response.json();
// POST /assets returns the reference id nested as mediaGenerationId.mediaGenerationId
const mediaGenerationId = json?.mediaGenerationId?.mediaGenerationId;
console.log(`π mediaGenerationId (${elapsedTimeSec(startTime)} sec)`, mediaGenerationId);
uploadedFiles[filename] = mediaGenerationId;
}
else {
console.error(`β Unable to upload file HTTP ${response.status} (${elapsedTimeSec(startTime)} sec)`, await response.text());
// Do not attempt to upload failed file again
uploadedFiles[filename] = undefined;
}
return uploadedFiles[filename];
}
async function submit(apiToken, url, body, index, prompt) {
const createResponse = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`
},
body
});
const createBody = await createResponse.text();
// Async POST /videos returns 201 Created with a jobid to poll.
if (createResponse.status == 201) {
const json = JSON.parse(createBody);
// 201 async payload uses lowercase "jobid"; sync 200 uses "jobId". Accept either.
const jobId = json.jobid ?? json.jobId;
if (jobId) {
await fs.appendFile(RESULTS_FILE, `${jobId},#${index}:${prompt}\n`);
console.log(`β
jobId`, jobId);
return 201;
} else {
const error = `No jobid found in HTTP 201 response`;
console.log(`β ${error}`, createBody);
await fs.appendFile(ERRORS_FILE, `${error},#${index}:${prompt}\n`);
return 500;
}
} else {
switch (createResponse.status) {
case 429:
console.log(`ποΈ Retry on HTTP ${createResponse.status}`, createBody);
break;
case 503:
console.log(`ποΈ Service unavailable, retry on HTTP ${createResponse.status}`, createBody);
break;
case 400:
console.log(`π Rejected request (validation or content policy)`, createBody);
await fs.appendFile(ERRORS_FILE, `${createResponse.status},#${index}:${prompt}\n`);
break;
case 402:
console.log(`π No subscription / insufficient credits`, createBody);
break;
default:
console.log(`β FAILED with HTTP ${createResponse.status}`, createBody);
await fs.appendFile(ERRORS_FILE, `${createResponse.status},#${index}:${prompt}\n`);
}
return createResponse.status;
}
}
// Submit a single prompt to POST /videos in async mode.
// startImage = start frame (I2V), endImage = end frame (I2V-FL, Veo only, requires startImage).
async function submitVideo(apiToken, email, prompt, index) {
const { model, prompt: text, startImage, endImage, aspectRatio, duration, count, seed } = prompt;
const useModel = model ?? DEFAULT_MODEL;
console.log(`π ${useModel} Β» Prompt #${index} β’ account ${email} β¦`);
const startImageId = startImage ? await uploadAsset(apiToken, email, startImage) : undefined;
const endImageId = endImage ? await uploadAsset(apiToken, email, endImage) : undefined;
const body = JSON.stringify({
model: useModel,
email,
prompt: text,
aspectRatio,
duration,
count,
seed,
startImage: startImageId,
endImage: endImageId,
async: true
});
return await submit(apiToken, urlVideos, body, index, text);
}
// Function to download videos
async function download(apiToken) {
if (! await fileExists(RESULTS_FILE)) return;
try {
const resultsContent = await fs.readFile(RESULTS_FILE, 'utf8');
const lines = resultsContent.trim().split('\n');
for (const line of lines) {
const [jobId, prompt] = line.split(',');
console.log(`π ${jobId}`);
while (true) {
const response = await fetch(`${urlJobs}${jobId}`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
});
if (!response.ok) {
console.log(`π Poll failed ${jobId} (HTTP ${response.status}):\n${prompt}\n`, await response.text());
break;
}
const jobBody = await response.json();
const { status, error, response: jobResponse } = jobBody;
if (status == 'failed') {
console.error(`π FAILED ${jobId} (${error}):\n${prompt}\n`);
break;
}
if (status == 'completed') {
const media = jobResponse?.media ?? [];
if (media.length == 0)
console.error(`π Completed but no media for ${jobId}:\n${prompt}\n`);
// A single prompt with count > 1 returns multiple videos.
for (let i = 0; i < media.length; i++) {
const url = media[i]?.videoUrl;
const videoFilename = `${jobId.replace(/[:*]/g, '_')}_${i + 1}.mp4`;
try {
await fs.access(videoFilename);
console.log(`β οΈ ${videoFilename} already exists. Skipping download.`);
continue;
} catch {
// File does not exist, proceed with downloading
}
if (url) {
console.log(`β
Downloading ${url} to ${videoFilename}`);
try {
const videoResponse = await fetch(url);
if (!videoResponse.ok) {
console.error(`β Unable to download ${jobId} (HTTP ${videoResponse.status}):\n${prompt}\n`, url);
continue;
}
const stream = Readable.fromWeb(videoResponse.body);
await writeFile(videoFilename, stream);
} catch (err) {
console.error(`β Error during download: ${err}`);
}
} else
console.error(`π No videoUrl for ${jobId} media #${i + 1}:\n${prompt}\n`);
}
break;
}
console.log(`β ${jobId} status (${status}) and is still in progress, waitingβ¦`);
await sleep(SLEEP_POLL);
}
}
} catch (error) {
console.log(`β Error during download:`, error.stack || error);
}
}
// Main function
async function main() {
const apiToken = process.argv[2];
const email = process.argv[3];
const promptFile = process.argv[4] || DEFAULT_PROMPTS_FILE;
if (!apiToken || !email) {
console.error('Usage: node google-flow.mjs <API_TOKEN> <EMAIL> [PROMPTS_FILE]');
process.exit(1);
}
console.info('Script v1.0');
console.info('Node version is: ' + process.version);
try {
if (await fileExists(RESULTS_FILE)) {
let user_input;
while (!['y', 'n'].includes(user_input)) {
user_input = (await promptUser(`β ${RESULTS_FILE} file detected. Do you want to download the results now? (y/n): `))?.toLowerCase();
if (user_input == 'y') {
await download(apiToken);
await fs.unlink(RESULTS_FILE);
}
}
}
const start = new Date();
try {
console.info('START EXECUTION', start);
await execute(apiToken, email, promptFile); // Pass the promptFile to execute function
}
finally {
console.info('COMPLETED', new Date());
console.info('EXECUTION ELAPSED', diffInMinutesAndSeconds(start, new Date()));
}
try {
console.info('START DOWNLOAD', start);
await download(apiToken);
}
finally {
console.info('TOTAL ELAPSED', diffInMinutesAndSeconds(start, new Date()));
}
} catch (error) {
console.error('β Error during execution:', error.stack || error);
}
}
// Modify the execute function to accept promptFile as a parameter
async function execute(apiToken, email, promptFile) {
const accounts = await fetchAccounts(apiToken);
console.info(`Configured Google Flow API accounts (${Object.keys(accounts).length}):`, Object.keys(accounts).join(', '));
if (Object.keys(accounts).length <= 0) {
console.error(`β No configured Google Flow accounts found. Please refer to https://useapi.net/docs/start-here/setup-google-flow`);
process.exit(1);
}
if (!accounts[email]) {
console.error(`β Account ${email} not found. Please refer to https://useapi.net/docs/start-here/setup-google-flow`);
process.exit(1);
}
if (accounts[email].health && accounts[email].health !== 'OK') {
console.error(`β Account ${email} health is '${accounts[email].health}'. Please resolve and update the account, see https://useapi.net/docs/start-here/setup-google-flow`);
process.exit(1);
}
const promptData = await fs.readFile(promptFile, 'utf8');
const prompts = JSON.parse(promptData);
console.log(`Total number of prompts to process`, prompts.length);
let warnings = [];
// Parameters accepted by this script for the POST /videos endpoint.
// See https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos for every model's full parameter set.
const supportedParams = ['model', 'prompt', 'startImage', 'endImage', 'aspectRatio', 'duration', 'count', 'seed'];
const invalidKeys = (prompt) => Object.keys(prompt).filter(key => !key.startsWith('__') && !supportedParams.includes(key))
for (let i = 1; i <= prompts.length; i++) {
const prompt = prompts[i - 1];
const { prompt: text, startImage, endImage } = prompt;
const validateImage = async (file) => {
if (file) {
try {
await fs.access(file);
} catch {
warnings.push(`β οΈ Image '${file}' does not exist. Prompt ${i}`);
}
const ext = file.split('.').pop();
if (!supportedFileExtensions.includes(ext))
warnings.push(`β οΈ Image ${file} extension ${ext} not supported. Prompt ${i}`);
}
};
const notSupported = invalidKeys(prompt);
if (notSupported.length)
warnings.push(`β οΈ Following params not supported: ${notSupported.join(',')}. Prompt ${i}`);
if (!text)
warnings.push(`β οΈ prompt is required. Prompt ${i}`);
if (endImage && !startImage)
warnings.push(`β οΈ endImage requires startImage (end-frame-only is not supported). Prompt ${i}`);
await Promise.all([validateImage(startImage), validateImage(endImage)]);
}
if (warnings.length > 0) {
warnings.forEach(warning => console.warn(warning));
console.error(`β Execution stopped due to warnings.`);
process.exit(1);
}
for (let i = 0; i < prompts.length; i++) {
const prompt = prompts[i];
while (true) {
const responseCode = await submitVideo(apiToken, email, prompt, i + 1);
if (responseCode == 429 || responseCode == 503)
await sleep(SLEEP_429);
else
if (responseCode == 402) {
process.exit(1);
} else
break;
}
}
}
// Utility function to check if a file exists
async function fileExists(path) {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
// Function to prompt user input
async function promptUser(query) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => rl.question(query, answer => {
rl.close();
resolve(answer);
}));
}
function diffInMinutesAndSeconds(date1, date2) {
const diffInSeconds = Math.floor((date2 - date1) / 1000);
return `${Math.floor(diffInSeconds / 60)} minutes ${diffInSeconds % 60} seconds`;
};
main();
Examples
Real Veo 3.1 clips generated through this Google Flow API, taken straight from our release notes.
veo-3.1-fast Β» Brave Captain Cat chasing away the pirates (image-to-video)
From Google Flow API v1: Initial Release. Animated from a Nano Banana still passed as startImage.
veo-3.1-fast Β» Lady and her dog, with AI voice narration
From Veo 3.1 Lite and Voice Narration. A reference-to-video generation with the referenceAudio_1: "Zephyr" preset adding spoken dialogue.
veo-3.1-fast Β» Dancing cha-cha (portrait reference-to-video)
From Veo 3.1 Portrait R2V. Portrait R2V from a single reference image.
Frequently asked questions
- Is there a Veo 3.1 API? Yes β two ways. Google offers Veo 3.1 directly through the official Gemini API (and Vertex AI), billed per second on a Google Cloud project. Or use useapi.netβs Google Flow API, which gives you programmatic access to Veo 3.1 (Quality, Fast, Lite) by driving your own Google Flow account through a standard REST endpoint β no Cloud project, and you pay your flat Flow subscription instead of metered per-second rates (often cheaper per clip β see Pricing).
- Whatβs the difference between Veo 3.1 Quality, Fast, and Lite? They trade quality for speed and cost:
veo-3.1-qualityis the highest-fidelity model,veo-3.1-fastis the default balance, andveo-3.1-liteis the cheapest, withveo-3.1-lite-low-priorityavailable to Ultra subscribers at lower scheduling priority. See Supported models above for per-model credits and durations. - Can I do image-to-video with Veo? Yes. Upload your start frame with POST /assets/
email, then pass the returned reference id (nested atmediaGenerationId.mediaGenerationId) asstartImageβ and optionallyendImagefor a firstβlast transition (Veo only, requiresstartImage). See Image-to-video above. - Do I need to solve a captcha? Google Flow video generation requires reCAPTCHA, but you do not solve it yourself. The useapi.net worker solves the captcha automatically β your first Google Flow account comes with 100 free captcha credits as a one-time grant (powered by CapSolver), and after that you configure your own provider keys via POST /accounts/captcha-providers. The script in this guide sends no
captchaTokenand relies on this automatic solving. - How much does it cost? You keep your paid Google AI subscription (needed for video) and add a flat useapi.net subscription for API access to all services. Each generation then spends your normal Flow credits from your Google plan β far cheaper than the official metered API; see Pricing above.
- How is this different from the official Google / Gemini API? Googleβs official Veo 3.1 access runs through the Gemini API / Vertex AI on a Google Cloud project, billed per second. Google Flow β the consumer app β has no public API of its own. useapi.net bridges that gap by driving your own consumer Flow account at its flat subscription price β no Cloud project or per-second metering (see the side-by-side Pricing comparison above), and the same subscription covers other AI services too.
Conclusion
Visit our Discord Server or
Telegram Channel for any support questions and concerns.
We regularly post guides and tutorials on the YouTube Channel.
Check our GitHub repo with code examples.