Veo 3.1 API の使い方:Google Flow API と curl で動画を生成する
6分で読めます • 2026年6月15日(2026年11月6日更新)
useapi.net はサードパーティの API です。Google とは提携しておらず、Google の承認も受けていません。
目次
はじめに
Veo 3.1 は、useapi.net の Google Flow API を通して、curl 1回で動きます。使うのは、お使いの Google Flow の Web サイトのプラン(Google AI Pro または Ultra)です。8秒の Veo 3.1 Fast の動画は Google AI Ultra $199.99 で $0.08 で、公式の Veo API では $0.80 です(2026年11月時点)。
Google は2026年10月22日に Gemini API の Veo 3.1 Preview モデルを終了し、開発者を Cloud の Agent Platform に移しました。Agent Platform は、同じ1秒あたりの料金を Google Cloud プロジェクトに請求します。このサードパーティの API なら、Cloud プロジェクトも Google の API キーも企業向けの承認も不要です。以下では、モデル、2回の呼び出しで終わる流れ、画像の入力、そのまま動く一括生成スクリプトを紹介します。
対応モデル
モデルはリクエストごとに model フィールドで選びます(デフォルトは veo-3.1-fast)。Veo のクレジット数は Google の公式の Flow クレジット表によるもので、Omni のクレジット数は実際の API で測定した値です。
| モデル ID | 種類・速度 | 長さ | 動画1本のクレジット |
|---|---|---|---|
veo-3.1-lite | 最も安い Veo | 4秒、6秒、8秒 | 10(Ultra 以外)・5(Ultra) |
veo-3.1-lite-low-priority | Lite、低優先度 | 4秒、6秒、8秒 | 0、Ultra $199.99 のプランのオーナーのみ |
veo-3.1-fast (デフォルト) | 高速な Veo | 4秒、6秒、8秒 | 20(Ultra 以外)・10(Ultra) |
veo-3.1-quality | 最高品質の Veo | 8秒のみ | 100 |
omni-flash-1.2 | Omni 1.2 Flash、音声ネイティブ | 3〜15秒 | 10秒までは1秒3、それ以降は1秒6(720p) |
omni-flash | Omni 1.1 Flash、音声ネイティブ | 4秒、6秒、8秒、10秒 | 7 / 10 / 12 / 15(長さ別、720p) |
Veo の4秒と6秒の動画には Ultra が必要です(概要を参照)。Google Flow では、別のエンドポイント POST /images で Nano Banana のモデルを使った静止画も作れます。
料金
動画は1本ごとに Google AI のプランの Flow クレジットを使い、useapi.net が動画ごとに料金を取ることはありません。各プランでの動画1本の料金と公式 API との比較は Google Flow の料金表に、Veo の詳しい料金は Veo 3.1 API の料金比較(英語)にあります。
2回の API 呼び出しで動画を生成する
useapi.net の API トークンと、接続済みの Google Flow アカウントが必要です。動画のジョブは非同期です。async: true を渡すと作成の呼び出しはすぐに jobid を返すので、動画ができるまでポーリングします。
- ジョブを送信する —
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
}'
非同期モードでは、すぐに 201 Created の応答が返り、小文字の jobid と 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
}
}
- 結果をポーリングする —
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 は created → started → completed と進みます。completed になると、完成した MP4 が response.media[].videoUrl に入っています(1つのプロンプトで count > 1 を指定すると、動画ごとに1件ずつ返ります)。
{
"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 は、モデルとモードによって通常60〜180秒で完了します。2026年10月20日の当社の実行では、8秒の動画9本が、送信から MP4 の完成まで50〜105秒でした(測定した実行、英語)。署名付きの videoUrl は約6時間有効なので、早めにダウンロードしてください。ポーリングしたくない場合は、作成のボディに replyUrl を渡すと、ジョブの完了時に Webhook で通知を受け取れます。
画像から動画、参照画像から動画
Veo は2つの方法で画像を入力として受け取ります。まず各画像を POST /assets/email でアップロードします(生のバイト列、画像の Content-Type、20 MB までの PNG/JPEG/WebP)。アップロードの応答では、ID が mediaGenerationId.mediaGenerationId に入れ子になっています。この入れ子の文字列を次のリクエストに渡します。画像を入力に指定した場合は email を省略でき、リクエストは画像をアップロードしたアカウントに送られます。
開始・終了フレーム(I2V)
画像は、そのまま動画の1フレームになります。startImage として渡すと、動画はそのフレームから始まります。
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
}'
endImage(2つ目のアップロードした参照 ID)を追加すると最後のフレームが固定され、Veo は2つのフレームの間をつなぐ動画を作ります。endImage には startImage が必要で、終了フレームだけの指定はできません。Omni のモデル(omni-flash-1.2、omni-flash)も同じ組み合わせを受け付けますが、その場合は2つのフレームが唯一の入力になり、referenceImage_*、character_*、referenceAudio_* は一緒に使えません。
参照画像(R2V)
画像は特定のフレームではなく、被写体、キャラクター、スタイルなど動画全体の方向を決めます。referenceImage_1 から referenceImage_3 までを渡します(Veo は最大3枚で、8秒の動画のみ、veo-3.1-quality では使えません)。R2V は 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
}'
プロンプトの中では、各参照をインラインの @referenceImage_N で指定します。ナレーションには referenceAudio_1(Zephyr のようなシステムの声の名前)を追加でき、再利用できるキャラクターを character_1 として渡すこともできます(同じ R2V モードで動き、3つの参照の枠に数えられます)。Omni Flash では参照をさらに広く使えます。最大7枚の参照、動画から動画への編集、同期した声に対応しています。Omni Flash のチュートリアル(英語)を参照してください。
スクリプトで一括生成する
狙いどおりのショットを得るには何度も試す必要があり、手作業で1本ずつ実行するのは面倒です。下の Node.js スクリプトは、prompts.json からプロンプトのリストを読み込み、開始・終了フレームの画像をアップロードし、各ジョブを非同期モードで送信してから、完成した MP4 をポーリングしてすべてダウンロードします。まとめてキューに入れておき、あとで良いものを選べます。
Node.js v21 以降が必要です。prompts.json と google-flow.mjs を同じフォルダーに置き、node ./google-flow.mjs API_TOKEN EMAIL を実行します。API_TOKEN は useapi.net の API トークン、EMAIL は接続済みの Google Flow アカウントのメールアドレスです。スクリプトはメールアドレスからアカウントを自動で探し、送信前に health フィールドを確認します。
クローンしてローカルで実行したい場合は、google-flow.mjs と prompts.json の完全版が GitHub の useapi/google-flow-api にあります。
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."
}
]
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();
例
この Google Flow API で作った実際の Veo 3.1 の動画です。当社のリリースノートからそのまま転載しています。
veo-3.1-fast » 海賊を追い払う勇敢なキャプテン・キャット(画像から動画)
出典は Google Flow API v1: Initial Release(英語)です。Nano Banana の静止画を startImage として渡し、動かしました。
veo-3.1-fast » 女性と犬、AI 音声のナレーション付き
出典は Veo 3.1 Lite and Voice Narration(英語)です。参照画像から動画を作り、referenceAudio_1: "Zephyr" のプリセットでセリフを加えました。
veo-3.1-fast » チャチャを踊る(縦長、参照画像から動画)
出典は Veo 3.1 Portrait R2V(英語)です。参照画像1枚から縦長の R2V で作りました。
よくある質問
- Veo 3.1 の API はありますか? useapi.net を使えば、あります。Google Flow API は、お使いの Google Flow の Web サイトのプラン(Google AI Pro または Ultra)で、Veo 3.1 Quality、Fast、Lite を REST で動かします。8秒の Veo 3.1 Quality の動画は Google AI Ultra $199.99 で $0.80 で、公式の Veo API では $3.20 です(2026年11月時点)。
- Veo 3.1 の Quality、Fast、Lite の違いは? useapi.net を使えば、リクエストごとに
modelフィールドで1つを選べます。veo-3.1-qualityは最も高い品質、veo-3.1-fastは速度と品質のバランスを取ったデフォルト、veo-3.1-liteは最も安く、Google AI Ultra $199.99 で1本 $0.04、公式の Veo API では $0.40 です。このプランではveo-3.1-lite-low-priorityが $0 で、1日約1,000本以上作れます(2026年11月時点)。 - Veo で画像から動画を作れますか? useapi.net を使えば、作れます。POST /assets/
emailで開始フレームをアップロードし、返ってきた参照 ID(mediaGenerationId.mediaGenerationIdに入れ子)をstartImageとして渡します。最後のフレームを固定するならendImageも渡します(startImageが必要です)。フレームではなく被写体やスタイルを指定したい場合は、referenceImage_Nを最大3枚渡します(2026年11月時点)。上の画像から動画を参照してください。 - CAPTCHA を解く必要はありますか? useapi.net を使えば、ご自身で解く必要はありません。Google Flow は動画ごとに reCAPTCHA を求めますが、useapi.net のワーカーが自動で解決します。最初の Google Flow アカウントには、当社の AntiCaptcha、CapSolver、EzCaptcha のキーで300回の無料解決が付き、その後は POST /accounts/captcha-providers でご自身のプロバイダーのキーを追加します(2026年11月時点)。このガイドのスクリプトは
captchaTokenを送りません。 - Veo 3.1 の API キーは必要ですか? useapi.net を使えば、Google の API キーも Google Cloud プロジェクトも不要です。useapi.net の API トークン1つで Veo 3.1 を呼び出せ、Google アカウントは自動セットアップで一度接続するだけです。Google Flow 自体には公開 API がなく、2026年10月22日に Gemini API の Veo 3.1 Preview モデルが終了したため、Google 自身の Veo API には課金を有効にした Cloud プロジェクトが必要です。
おわりに
ご質問は Discord サーバー または
Telegram チャンネルへどうぞ。
コード例は GitHub リポジトリにあります。
ほかの掲載先
この記事(英語版)は dev.to にも掲載しています。
API を使い始める
月額15ドルのサブスクリプション1つで、useapi.net のすべての API を使えます。トークン1つで、すべてのサービスに対応します。各サービスにご自身のアカウントを最大3つまで接続でき、REST API、負荷分散、ポーリングは useapi.net が担当します。アカウントを増やしたい場合は、サブスクリプションを1つ追加するごとに月額10ドルで、各サービスのアカウントが3つずつ増えます。
海外で使えるクレジットカードがない場合は、暗号資産で登録することもできます(暗号資産での支払いは返金できません)。料金とボリュームプランはサブスクリプションのページをご覧ください。Stripe でお支払いの場合、初回購入から14日以内で、成功した生成が50回未満であれば全額返金されます。すでに登録済みの場合は、新たに購入せず Stripe カスタマーポータルで数量を変更してください。