Cách dùng Veo 3.1 API: tạo video bằng curl qua Google Flow API
6 phút đọc • 15 tháng 6 năm 2026 (6 tháng 11 năm 2026)
useapi.net là API của bên thứ ba, không liên kết với Google và không được Google bảo trợ.
Mục lục
- Giới thiệu
- Các model được hỗ trợ
- Giá
- Tạo video bằng hai lệnh gọi API
- Tạo video từ ảnh và từ ảnh tham chiếu
- Tạo hàng loạt bằng script
- Ví dụ
- Câu hỏi thường gặp
- Kết luận
Giới thiệu
Veo 3.1 chạy chỉ với một lệnh curl qua Google Flow API của useapi.net, trên gói đăng ký website Google Flow của bạn (Google AI Pro hoặc Ultra). Một video Veo 3.1 Fast dài 8 giây tốn $0,08 trên Google AI Ultra $199,99, so với $0,80 trên Veo API chính thức (tính đến tháng 11/2026).
Google đã tắt các model Veo 3.1 Preview trên Gemini API ngày 22 tháng 10 năm 2026 và chuyển nhà phát triển sang Agent Platform trên Cloud, nơi tính cùng mức giá theo giây trên một dự án Google Cloud. API bên thứ ba này không cần dự án Cloud, không cần Google API key và không cần phê duyệt doanh nghiệp. Bên dưới: các model, quy trình hai lệnh gọi, đầu vào là ảnh và một script chạy hàng loạt.
Các model được hỗ trợ
Chọn model cho mỗi request bằng trường model (mặc định veo-3.1-fast). Số credit của Veo lấy từ bảng credit Flow chính thức của Google, số của Omni được đo trên API thật:
| Model id | Hạng / tốc độ | Độ dài | Credit mỗi video |
|---|---|---|---|
veo-3.1-lite | Veo rẻ nhất | 4s, 6s, 8s | 10 (ngoài Ultra) · 5 (Ultra) |
veo-3.1-lite-low-priority | Lite, ưu tiên thấp | 4s, 6s, 8s | 0, chỉ chủ gói Ultra $199,99 |
veo-3.1-fast (mặc định) | Veo nhanh | 4s, 6s, 8s | 20 (ngoài Ultra) · 10 (Ultra) |
veo-3.1-quality | Veo chất lượng cao nhất | chỉ 8s | 100 |
omni-flash-1.2 | Omni 1.2 Flash, có âm thanh gốc | 3s đến 15s | 3 mỗi giây tới 10s, 6 mỗi giây sau đó (ở 720p) |
omni-flash | Omni 1.1 Flash, có âm thanh gốc | 4s, 6s, 8s, 10s | 7 / 10 / 12 / 15 (theo độ dài, ở 720p) |
Với Veo, video 4s và 6s cần Ultra (xem trang tổng quan). Google Flow cũng tạo ảnh tĩnh bằng các model Nano Banana qua endpoint riêng POST /images.
Giá
Mỗi video dùng credit Flow từ gói Google AI của bạn, và useapi.net không thu phí theo video. Giá mỗi video trên từng gói, so với API chính thức, có trong bảng giá Google Flow, còn bài giá Veo 3.1 API nói chi tiết về Veo.
Tạo video bằng hai lệnh gọi API
Bạn cần một API token của useapi.net và một tài khoản Google Flow đã kết nối. Job video chạy bất đồng bộ: truyền async: true và lệnh tạo trả về jobid ngay, sau đó bạn polling cho tới khi video sẵn sàng.
- Gửi 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
}'
Ở chế độ async, phản hồi trả về ngay với 201 Created, một jobid viết thường và 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
}
}
- Polling lấy kết quả —
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 chuyển từ created → started → completed. Khi tới completed, file MP4 hoàn chỉnh nằm ở response.media[].videoUrl (một prompt với count > 1 trả về một mục cho mỗi video):
{
"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 thường xong trong 60–180 giây tùy model và chế độ. Trong các lần chạy của chúng tôi ngày 20 tháng 10 năm 2026, chín video 8 giây mất 50 đến 105 giây từ lúc gửi tới khi có MP4 hoàn chỉnh (các lần chạy đã đo). videoUrl có chữ ký dùng được khoảng 6 giờ, nên hãy tải về sớm. Không muốn polling? Truyền replyUrl trong body của lệnh tạo để nhận webhook khi job xong.
Tạo video từ ảnh và từ ảnh tham chiếu
Veo nhận ảnh đầu vào theo hai cách khác nhau. Trước tiên tải từng ảnh lên bằng POST /assets/email (byte thô, Content-Type ảnh, PNG/JPEG/WebP tối đa 20 MB). Phản hồi tải lên đặt id lồng ở mediaGenerationId.mediaGenerationId — chuỗi lồng đó là thứ bạn truyền lại. Khi có bất kỳ ảnh đầu vào nào, bạn có thể bỏ email: request sẽ đi tới tài khoản đã nhận ảnh tải lên.
Khung đầu / khung cuối (I2V)
Ảnh của bạn trở thành đúng một khung hình của video. Truyền nó làm startImage để video bắt đầu từ khung đó:
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
}'
Thêm endImage (id tham chiếu của một ảnh tải lên thứ hai) để cố định khung cuối, và Veo tạo đoạn chuyển giữa hai khung. endImage cần có startImage — không hỗ trợ chỉ có khung cuối. Các model Omni (omni-flash-1.2, omni-flash) nhận cùng cặp này, nhưng khi đó hai khung là đầu vào duy nhất — không kèm referenceImage_*, character_* hay referenceAudio_*.
Ảnh tham chiếu (R2V)
Ảnh của bạn định hướng cả video — chủ thể, nhân vật hoặc phong cách — thay vì một khung hình cụ thể. Truyền từ referenceImage_1 tới referenceImage_3 (Veo nhận tối đa 3, chỉ với video 8 giây, và không dùng với veo-3.1-quality). R2V không kết hợp được với 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
}'
Nhắc tới từng tham chiếu ngay trong prompt bằng dấu @referenceImage_N, thêm referenceAudio_1 (tên một giọng hệ thống như Zephyr) để có lời thuyết minh, hoặc truyền một nhân vật dùng lại được làm character_1 (nó chạy cùng chế độ R2V và tính vào giới hạn 3 tham chiếu). Omni Flash đi xa hơn với tham chiếu — tối đa 7, cộng với chỉnh sửa video sang video và giọng nói đồng bộ — xem hướng dẫn Omni Flash.
Tạo hàng loạt bằng script
Tìm được cảnh quay ưng ý cần nhiều lần thử, và chạy từng lần bằng tay rất mất công. Script Node.js dưới đây đọc danh sách prompt từ prompts.json, tải lên ảnh khung đầu/khung cuối nếu có, gửi từng job ở chế độ async, rồi polling và tải về mọi file MP4 đã xong — nên bạn có thể xếp hàng cả loạt rồi quay lại chọn video đẹp nhất.
Bạn cần Node.js v21 trở lên. Đặt prompts.json và google-flow.mjs cùng một thư mục và chạy node ./google-flow.mjs API_TOKEN EMAIL, trong đó API_TOKEN là API token useapi.net của bạn và EMAIL là email của tài khoản Google Flow đã kết nối. Script tự tìm tài khoản theo email và kiểm tra trường health trước khi gửi.
Muốn clone và chạy trên máy? File google-flow.mjs và prompts.json đầy đủ có trên GitHub ở useapi/google-flow-api.
Mở 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). Supported by Veo and by both Omni models."
},
{
"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."
}
]
Mở script google-flow.mjs
/*
Script version 1.1, September 11, 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.
- September 11, 2026: Accepts .jpg and upper-case image extensions for startImage and endImage. A prompt whose startImage or endImage fails to upload is skipped and logged to google-flow_errors.txt instead of being submitted without that frame. Saves a downloaded video under its final name only once it has been fully received, so a failed download leaves no partial MP4 for the next run to skip as "already exists".
*/
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', 'jpg', '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().toLowerCase();
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, 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;
// A frame that failed to upload would be left out of the body, and the prompt would still be
// generated (and charged) without it. Skip the prompt instead. uploadAsset remembers the failed file,
// so a later prompt using it is skipped too, without another upload attempt.
const failedUploads = [['startImage', startImage, startImageId], ['endImage', endImage, endImageId]]
.filter(([, file, id]) => file && !id);
for (const [param, file] of failedUploads) {
console.error(`🛑 Prompt #${index} skipped: ${param} ${file} failed to upload`);
await fs.appendFile(ERRORS_FILE, `Upload failed for ${param} ${file},#${index}:${text}\n`);
}
if (failedUploads.length)
return;
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}`);
// Stream into a temporary file and rename it once the whole video has arrived, so a download
// that drops mid-stream leaves no partial file behind for the next run to skip as "already exists".
const tempFilename = `${videoFilename}.part`;
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(tempFilename, stream);
await fs.rename(tempFilename, videoFilename);
} catch (err) {
await fs.rm(tempFilename, { force: true });
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.1');
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().toLowerCase();
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();
Ví dụ
Video Veo 3.1 thật làm qua Google Flow API này, lấy thẳng từ các bài thông báo phát hành của chúng tôi.
veo-3.1-fast » Thuyền trưởng Mèo dũng cảm đuổi bọn cướp biển (tạo video từ ảnh)
Từ Google Flow API v1: Initial Release. Làm chuyển động từ một ảnh tĩnh Nano Banana truyền vào làm startImage.
veo-3.1-fast » Người phụ nữ và chú chó, có lời thuyết minh bằng giọng AI
Từ Veo 3.1 Lite and Voice Narration. Tạo video từ ảnh tham chiếu với preset referenceAudio_1: "Zephyr" thêm lời thoại.
veo-3.1-fast » Nhảy cha-cha (tạo video dọc từ ảnh tham chiếu)
Từ Veo 3.1 Portrait R2V. R2V khổ dọc từ một ảnh tham chiếu duy nhất.
Câu hỏi thường gặp
- Veo 3.1 có API không? Qua useapi.net thì có: Google Flow API chạy Veo 3.1 Quality, Fast và Lite qua REST trên gói đăng ký website Google Flow của bạn (Google AI Pro hoặc Ultra). Một video Veo 3.1 Quality dài 8 giây tốn $0,80 trên Google AI Ultra $199,99, so với $3,20 trên Veo API chính thức (tính đến tháng 11/2026).
- Veo 3.1 Quality, Fast và Lite khác nhau thế nào? Qua useapi.net, bạn chọn một model cho mỗi request bằng trường
model.veo-3.1-qualitycho chất lượng cao nhất,veo-3.1-fastlà mặc định cân bằng giữa tốc độ và chất lượng, cònveo-3.1-literẻ nhất: $0,04 một video trên Google AI Ultra $199,99, so với $0,40 trên Veo API chính thức. Trên gói đó,veo-3.1-lite-low-prioritytốn $0, khoảng 1.000 video trở lên mỗi ngày (tính đến tháng 11/2026). - Veo có tạo video từ ảnh được không? Qua useapi.net thì được. Tải khung đầu lên bằng POST /assets/
email, rồi truyền id tham chiếu nhận được (lồng ởmediaGenerationId.mediaGenerationId) làmstartImage, và tùy chọnendImageđể cố định khung cuối (cần cóstartImage). Để định hướng chủ thể hoặc phong cách thay vì một khung hình, truyền tối đa bareferenceImage_N. Xem Tạo video từ ảnh ở trên. - Tôi có phải tự giải captcha không? Qua useapi.net, bạn không bao giờ phải tự giải. Google Flow yêu cầu reCAPTCHA cho mỗi video, và worker của useapi.net tự động giải: tài khoản Google Flow đầu tiên của bạn được 300 lần giải miễn phí bằng key AntiCaptcha, CapSolver và EzCaptcha của chúng tôi, sau đó bạn thêm key dịch vụ của riêng mình bằng POST /accounts/captcha-providers. Script trong bài này không gửi
captchaToken. - Tôi có cần Veo 3.1 API key không? Qua useapi.net, bạn không cần Google API key hay dự án Google Cloud: một API token useapi.net gọi được Veo 3.1, và bạn kết nối tài khoản Google một lần qua thiết lập tự động. Bản thân Google Flow không có API công khai, và từ khi các model Veo 3.1 Preview trên Gemini API bị tắt ngày 22 tháng 10 năm 2026, Veo API của chính Google cần một dự án Cloud có bật thanh toán.
Kết luận
Ghé Discord Server hoặc
Telegram Channel của chúng tôi cho mọi câu hỏi hỗ trợ.
Xem repo GitHub của chúng tôi với các ví dụ code.
Đăng ở nơi khác
Cũng đăng trên dev.to (tiếng Anh).
Bắt đầu dùng API
Một gói đăng ký $15/tháng dùng được mọi API trên useapi.net: một token cho tất cả dịch vụ. Mỗi dịch vụ bạn kết nối tối đa 3 tài khoản của riêng mình, còn useapi.net lo REST API, cân bằng tải và polling. Cần thêm tài khoản? Mỗi gói đăng ký thêm giá $10/tháng và cho thêm 3 tài khoản ở mỗi dịch vụ.
Không có thẻ thanh toán quốc tế? Bạn có thể đăng ký bằng tiền mã hóa (thanh toán bằng tiền mã hóa không được hoàn tiền). Giá và các gói số lượng lớn có trên trang đăng ký. Nếu thanh toán qua Stripe, bạn được hoàn tiền toàn bộ trong 14 ngày kể từ lần mua đầu tiên khi có dưới 50 lượt tạo thành công. Đã đăng ký rồi? Hãy đổi số lượng trong Stripe Customer Portal thay vì mua lại.