Cara Memakai Veo 3.1 API: Membuat Video dengan curl lewat Google Flow API
6 menit baca β’ 15 Juni 2026 (6 November 2026)
useapi.net adalah API pihak ketiga. Kami tidak berafiliasi dengan Google dan tidak disahkan oleh Google.
Daftar isi
- Pendahuluan
- Model yang didukung
- Harga
- Membuat video dalam dua panggilan API
- Image-to-video dan reference-to-video
- Generate batch dengan skrip
- Contoh
- Pertanyaan yang sering diajukan
- Kesimpulan
Pendahuluan
Veo 3.1 berjalan dari satu curl lewat Google Flow API useapi.net, di langganan situs Google Flow Anda (Google AI Pro atau Ultra). Satu video Veo 3.1 Fast 8 detik berbiaya $0,08 di Google AI Ultra $199,99, dibanding $0,80 di Veo API resmi (per November 2026).
Google menghentikan model Veo 3.1 Preview di Gemini API pada 22 Oktober 2026 dan memindahkan developer ke Agent Platform di Cloud, yang menagih tarif per detik yang sama di project Google Cloud. API pihak ketiga ini tidak butuh project Cloud, API key Google, maupun persetujuan enterprise. Di bawah ini: model-modelnya, alur kerja dua panggilan, input gambar, dan skrip batch yang siap dijalankan.
Model yang didukung
Pilih model per request dengan field model (default veo-3.1-fast). Biaya kredit Veo berasal dari tabel kredit Flow resmi Google, dan biaya Omni diukur langsung di API:
| Id model | Tier / kecepatan | Durasi | Kredit per video |
|---|---|---|---|
veo-3.1-lite | Veo termurah | 4s, 6s, 8s | 10 (Non-Ultra) Β· 5 (Ultra) |
veo-3.1-lite-low-priority | Lite, prioritas rendah | 4s, 6s, 8s | 0, hanya pemilik paket Ultra $199,99 |
veo-3.1-fast (default) | Veo cepat | 4s, 6s, 8s | 20 (Non-Ultra) Β· 10 (Ultra) |
veo-3.1-quality | Veo kualitas tertinggi | hanya 8s | 100 |
omni-flash-1.2 | Omni 1.2 Flash, audio bawaan | 3s sampai 15s | 3 per detik hingga 10s, 6 per detik setelahnya (di 720p) |
omni-flash | Omni 1.1 Flash, audio bawaan | 4s, 6s, 8s, 10s | 7 / 10 / 12 / 15 (menurut durasi, di 720p) |
Di Veo, video 4s dan 6s butuh Ultra (lihat ringkasan API). Google Flow juga membuat gambar diam dengan model Nano Banana lewat endpoint terpisah POST /images.
Harga
Setiap video memakai kredit Flow dari langganan Google AI Anda, tanpa biaya per video dari useapi.net. Harga per video di setiap paket, dibandingkan dengan API resmi, ada di tabel harga Google Flow, dan harga API Veo 3.1 (bahasa Inggris) membahas Veo secara rinci.
Membuat video dalam dua panggilan API
Anda butuh API token useapi.net dan akun Google Flow yang sudah terhubung. Job video berjalan secara asynchronous: kirim async: true dan panggilan create langsung mengembalikan jobid, lalu Anda melakukan polling sampai videonya siap.
- Kirim 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
}'
Dalam mode async, respons langsung kembali dengan 201 Created, jobid berhuruf kecil, dan 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 hasilnya β
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 berpindah dari created β started β completed. Saat mencapai completed, MP4 jadi ada di response.media[].videoUrl (satu prompt dengan count > 1 mengembalikan satu entri per 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 biasanya selesai dalam 60β180 detik, tergantung model dan mode. Dalam uji kami pada 20 Oktober 2026, sembilan video 8 detik butuh 50 sampai 105 detik dari pengiriman sampai MP4 jadi (uji yang diukur). videoUrl yang ditandatangani berlaku sekitar 6 jam, jadi segera unduh. Tidak mau polling? Kirim replyUrl di body create untuk menerima callback webhook saat job selesai.
Image-to-video dan reference-to-video
Veo menerima input gambar dengan dua cara berbeda. Upload dulu setiap gambar dengan POST /assets/email (byte mentah, Content-Type gambar, PNG/JPEG/WebP hingga 20 MB). Respons upload menyimpan id di mediaGenerationId.mediaGenerationId β string bersarang itulah yang Anda kirim kembali. Jika Anda memberi input gambar apa pun, email boleh dihilangkan: request diarahkan ke akun tempat gambar itu di-upload.
Frame pertama / terakhir (I2V)
Gambar Anda menjadi frame sungguhan di video. Kirim sebagai startImage untuk memulai dari frame itu:
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
}'
Tambahkan endImage (id referensi upload kedua) untuk mengunci frame terakhir, sehingga Veo membuat transisi di antara keduanya. endImage butuh startImage β hanya frame akhir tidak didukung. Model Omni (omni-flash-1.2, omni-flash) menerima pasangan yang sama, tetapi di sana kedua frame itu satu-satunya input yang diterima β tanpa referenceImage_*, character_* atau referenceAudio_* di sampingnya.
Gambar referensi (R2V)
Gambar Anda memandu seluruh video β subjek, karakter atau gaya β bukan frame tertentu. Kirim referenceImage_1 sampai referenceImage_3 (Veo menerima hingga 3, hanya untuk video 8 detik, dan tidak di veo-3.1-quality). R2V tidak bisa digabung dengan 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
}'
Sebut setiap referensi secara inline dengan penanda @referenceImage_N di prompt, tambahkan referenceAudio_1 (nama suara sistem seperti Zephyr) untuk narasi, atau kirim karakter yang bisa dipakai ulang sebagai character_1 (karakter menjalankan mode R2V yang sama dan dihitung dalam batas 3 referensi). Omni Flash membawa referensi lebih jauh β hingga 7, ditambah edit video-ke-video dan suara yang tersinkron β lihat tutorial Omni Flash (bahasa Inggris).
Generate batch dengan skrip
Menemukan shot yang pas butuh banyak percobaan, dan menjalankannya satu per satu dengan tangan itu melelahkan. Skrip Node.js di bawah membaca daftar prompt dari prompts.json, meng-upload gambar frame awal/akhir jika ada, mengirim setiap job dalam mode async, lalu melakukan polling dan mengunduh setiap MP4 yang sudah jadi β jadi Anda bisa mengantrekan satu batch lalu kembali untuk memilih yang terbaik.
Anda butuh Node.js v21 atau lebih baru. Taruh prompts.json dan google-flow.mjs di folder yang sama dan jalankan node ./google-flow.mjs API_TOKEN EMAIL, dengan API_TOKEN berisi API token useapi.net Anda dan EMAIL berisi email akun Google Flow yang terhubung. Skrip mencari akun berdasarkan email secara otomatis dan memeriksa field health-nya sebelum mengirim.
Lebih suka clone dan menjalankannya di komputer sendiri? google-flow.mjs dan prompts.json lengkap ada di GitHub di useapi/google-flow-api.
Buka 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."
}
]
Buka skrip 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();
Contoh
Video Veo 3.1 asli yang dibuat lewat Google Flow API ini, langsung dari catatan rilis kami.
veo-3.1-fast Β» Kapten Kucing pemberani mengusir bajak laut (image-to-video)
Dari Google Flow API v1: Initial Release. Dianimasikan dari gambar diam Nano Banana yang dikirim sebagai startImage.
veo-3.1-fast Β» Seorang wanita dan anjingnya, dengan narasi suara AI
Dari Veo 3.1 Lite and Voice Narration. Reference-to-video dengan preset referenceAudio_1: "Zephyr" yang menambahkan dialog lisan.
veo-3.1-fast Β» Menari cha-cha (reference-to-video potret)
Dari Veo 3.1 Portrait R2V. R2V potret dari satu gambar referensi.
Pertanyaan yang sering diajukan
- Apakah ada API Veo 3.1? Lewat useapi.net, ada: Google Flow API menjalankan Veo 3.1 Quality, Fast dan Lite lewat REST di langganan situs Google Flow Anda (Google AI Pro atau Ultra). Satu video Veo 3.1 Quality 8 detik berbiaya $0,80 di Google AI Ultra $199,99, dibanding $3,20 di Veo API resmi (per November 2026).
- Apa beda Veo 3.1 Quality, Fast dan Lite? Lewat useapi.net Anda memilih salah satunya per request dengan field
model.veo-3.1-qualitymemberi kualitas tertinggi,veo-3.1-fastadalah default yang seimbang antara kecepatan dan kualitas, danveo-3.1-litepaling murah: $0,04 per video di Google AI Ultra $199,99, dibanding $0,40 di Veo API resmi. Di paket itu,veo-3.1-lite-low-priorityberbiaya $0, sekitar 1.000 video atau lebih per hari (per November 2026). - Bisakah membuat image-to-video dengan Veo? Lewat useapi.net, bisa. Upload frame awal Anda dengan POST /assets/
email, lalu kirim id referensi yang dikembalikan (bersarang dimediaGenerationId.mediaGenerationId) sebagaistartImage, dan jika perluendImageuntuk mengunci frame terakhir (butuhstartImage). Untuk memandu subjek atau gaya alih-alih frame, kirim hingga tigareferenceImage_N. Lihat Image-to-video di atas. - Apakah saya harus menyelesaikan captcha? Lewat useapi.net Anda tidak pernah menyelesaikannya sendiri. Google Flow meminta reCAPTCHA untuk setiap video, dan worker useapi.net menyelesaikannya otomatis: akun Google Flow pertama Anda mendapat 300 penyelesaian gratis dengan key AntiCaptcha, CapSolver dan EzCaptcha milik kami, dan setelah itu Anda menambahkan key penyedia sendiri dengan POST /accounts/captcha-providers. Skrip di panduan ini tidak mengirim
captchaToken. - Apakah saya butuh API key Veo 3.1? Lewat useapi.net Anda tidak butuh API key Google maupun project Google Cloud: satu API token useapi.net memanggil Veo 3.1, dan Anda menghubungkan akun Google sekali lewat setup otomatis. Google Flow sendiri tidak punya API publik, dan sejak model Veo 3.1 Preview di Gemini API dihentikan pada 22 Oktober 2026, Veo API milik Google butuh project Cloud dengan billing.
Kesimpulan
Kunjungi Server Discord atau
Channel Telegram kami untuk pertanyaan dan masalah dukungan.
Lihat repo GitHub kami berisi contoh kode.
Dipublikasikan juga di
Juga dipublikasikan di dev.to (bahasa Inggris).
Mulai memakai API
Satu langganan US$15/bulan mencakup semua API di useapi.net: satu token untuk semua layanan. Di setiap layanan Anda bisa menghubungkan hingga 3 akun milik Anda sendiri, dan useapi.net menangani REST API, load balancing, dan polling. Butuh lebih banyak akun? Setiap langganan tambahan US$10/bulan dan menambah 3 akun lagi per layanan.
Tidak punya kartu kredit internasional? Anda bisa berlangganan dengan kripto (pembayaran kripto tidak bisa di-refund). Harga dan paket volume ada di halaman langganan. Jika membayar lewat Stripe, Anda mendapat refund penuh dalam 14 hari sejak pembelian pertama, selama jumlah generasi yang berhasil kurang dari 50. Sudah berlangganan? Ubah jumlahnya di Stripe Customer Portal, tidak perlu membeli lagi.