วิธีใช้ Veo 3.1 API: สร้างวิดีโอด้วย curl ผ่าน Google Flow API

อ่าน 6 นาที • 15 มิถุนายน 2026 (6 พฤศจิกายน 2026)

useapi.net เป็น API ของบุคคลที่สาม (third-party) ไม่มีส่วนเกี่ยวข้องกับ Google และไม่ได้รับการรับรองจาก Google

สารบัญ

  1. บทนำ
  2. โมเดลที่รองรับ
  3. ราคา
  4. สร้างวิดีโอด้วยการเรียก API สองครั้ง
  5. วิดีโอจากภาพ และวิดีโอจากภาพอ้างอิง
    1. เฟรมแรก / เฟรมสุดท้าย (I2V)
    2. ภาพอ้างอิง (R2V)
  6. สร้างวิดีโอเป็นชุดด้วยสคริปต์
  7. ตัวอย่าง
  8. คำถามที่พบบ่อย
  9. สรุป

บทนำ

Veo 3.1 ทำงานได้ด้วย curl คำสั่งเดียวผ่าน Google Flow API ของ useapi.net บนแพ็กเกจเว็บไซต์ Google Flow ของคุณ (Google AI Pro หรือ Ultra) วิดีโอ Veo 3.1 Fast ยาว 8 วินาทีราคา $0.08 บน Google AI Ultra $199.99 เทียบกับ $0.80 บน Veo API ทางการ (ข้อมูล ณ พฤศจิกายน 2026)

Google ปิดโมเดล Veo 3.1 Preview บน Gemini API เมื่อ 22 ตุลาคม 2026 และย้ายนักพัฒนาไปที่ Agent Platform บน Cloud ซึ่งคิดราคาต่อวินาทีเท่าเดิมบนโปรเจกต์ Google Cloud API ของบุคคลที่สามนี้ไม่ต้องมีโปรเจกต์ Cloud ไม่ต้องมี Google API key และไม่ต้องรอการอนุมัติแบบองค์กร ด้านล่างนี้มีรายชื่อโมเดล ขั้นตอนเรียก API สองครั้ง การใส่ภาพ และสคริปต์สร้างวิดีโอเป็นชุดที่รันได้ทันที

โมเดลที่รองรับ

เลือกโมเดลในแต่ละคำขอด้วยฟิลด์ model (ค่าเริ่มต้น veo-3.1-fast) เครดิตของ Veo มาจากตารางเครดิต Flow ทางการของ Google ส่วนของ Omni วัดจาก API จริง:

Model id ระดับ / ความเร็ว ความยาว เครดิตต่อวิดีโอ
veo-3.1-lite Veo ที่ถูกที่สุด 4s, 6s, 8s 10 (ไม่ใช่ Ultra) · 5 (Ultra)
veo-3.1-lite-low-priority Lite ลำดับรอง 4s, 6s, 8s 0 เฉพาะเจ้าของแพ็กเกจ Ultra $199.99
veo-3.1-fast (ค่าเริ่มต้น) Veo แบบเร็ว 4s, 6s, 8s 20 (ไม่ใช่ Ultra) · 10 (Ultra)
veo-3.1-quality Veo คุณภาพสูงสุด 8s เท่านั้น 100
omni-flash-1.2 Omni 1.2 Flash มีเสียงในตัว 3s ถึง 15s 3 ต่อวินาทีถึง 10s, 6 ต่อวินาทีเมื่อเกิน (ที่ 720p)
omni-flash Omni 1.1 Flash มีเสียงในตัว 4s, 6s, 8s, 10s 7 / 10 / 12 / 15 (ตามความยาว ที่ 720p)

บน Veo วิดีโอ 4s และ 6s ต้องใช้ Ultra (ดูภาพรวม) Google Flow ยังสร้างภาพนิ่งด้วยโมเดล Nano Banana ผ่าน endpoint POST /images ที่แยกต่างหาก

ราคา

วิดีโอแต่ละคลิปใช้เครดิต Flow จากแพ็กเกจ Google AI ของคุณ และ useapi.net ไม่คิดเงินต่อวิดีโอ ราคาต่อวิดีโอบนทุกแพ็กเกจเทียบกับ API ทางการอยู่ในตารางราคา Google Flow และบทความราคา Veo 3.1 API อธิบายราคา Veo อย่างละเอียด

สร้างวิดีโอด้วยการเรียก API สองครั้ง

คุณต้องมี API token ของ useapi.net และบัญชี Google Flowที่เชื่อมไว้แล้ว งานวิดีโอเป็นแบบ asynchronous ส่ง async: true แล้วคำขอสร้างจะคืน jobid กลับมาทันที จากนั้นคุณ poll จนวิดีโอเสร็จ

  1. ส่งงาน — 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
  }'

ในโหมด async คำตอบกลับมาทันทีพร้อม 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
  }
}
  1. poll ผลลัพธ์ — 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 (prompt เดียวที่ใช้ count > 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 วินาที ขึ้นกับโมเดลและโหมด ในการรันของเราเมื่อ 20 ตุลาคม 2026 วิดีโอยาว 8 วินาที 9 คลิปใช้เวลา 50 ถึง 105 วินาทีตั้งแต่ส่งงานจนได้ MP4 (ผลการรันที่วัดไว้) videoUrl ที่มีลายเซ็นใช้ได้ราว 6 ชั่วโมง จึงควรดาวน์โหลดทันที ไม่อยาก poll? ใส่ replyUrl ใน body ของคำขอสร้าง เพื่อรับ webhook callback เมื่องานเสร็จ

วิดีโอจากภาพ และวิดีโอจากภาพอ้างอิง

Veo รับภาพได้สองแบบ อัปโหลดภาพแต่ละภาพก่อนด้วย POST /assets/email (ไบต์ดิบ, Content-Type แบบภาพ, PNG/JPEG/WebP ไม่เกิน 20 MB) คำตอบของการอัปโหลดเก็บ id ซ้อนไว้ที่ mediaGenerationId.mediaGenerationId สตริงที่ซ้อนอยู่นี้คือค่าที่คุณส่งกลับไป เมื่อส่งภาพใด ๆ ไปด้วย คุณไม่ต้องใส่ email ก็ได้ คำขอจะถูกส่งไปยังบัญชีที่อัปโหลดภาพนั้น

เฟรมแรก / เฟรมสุดท้าย (I2V)

ภาพของคุณจะกลายเป็นเฟรมหนึ่งของวิดีโอจริง ๆ ส่งเป็น 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 (id อ้างอิงของภาพที่อัปโหลดอีกภาพ) เพื่อกำหนดเฟรมสุดท้าย แล้ว Veo จะสร้างช่วงเปลี่ยนระหว่างสองภาพ endImage ต้องใช้คู่กับ startImage ไม่รองรับการใส่แค่เฟรมสุดท้าย โมเดล Omni (omni-flash-1.2, omni-flash) รับคู่เฟรมนี้ได้เหมือนกัน แต่สองเฟรมนี้จะเป็นอินพุตเดียวที่รับ ใส่ 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
  }'

อ้างถึงภาพอ้างอิงแต่ละภาพใน prompt ด้วยเครื่องหมาย @referenceImage_N เพิ่ม referenceAudio_1 (ชื่อเสียงของระบบ เช่น Zephyr) สำหรับเสียงบรรยาย หรือส่งตัวละครที่ใช้ซ้ำได้เป็น character_1 (ใช้โหมด R2V เดียวกัน และนับรวมในโควตาภาพอ้างอิง 3 ภาพ) Omni Flash ทำได้มากกว่านั้น รับภาพอ้างอิงได้สูงสุด 7 ภาพ พร้อมแก้ไขวิดีโอจากวิดีโอและเสียงพูดที่ซิงก์กัน ดูคู่มือ Omni Flash

สร้างวิดีโอเป็นชุดด้วยสคริปต์

กว่าจะได้ช็อตที่ใช่ต้องลองหลายครั้ง และการรันทีละครั้งด้วยมือก็น่าเบื่อ สคริปต์ Node.js ด้านล่างอ่านรายการ prompt จาก prompts.json อัปโหลดภาพเฟรมเริ่ม/เฟรมจบ ส่งงานแต่ละงานในโหมด async แล้ว poll และดาวน์โหลด MP4 ทุกไฟล์ที่เสร็จ คุณจึงส่งงานไว้เป็นชุด แล้วค่อยกลับมาเลือกคลิปที่ดีที่สุด

คุณต้องมี Node.js v21 ขึ้นไป วาง prompts.json และ google-flow.mjs ไว้ในโฟลเดอร์เดียวกัน แล้วรัน node ./google-flow.mjs API_TOKEN EMAIL โดย API_TOKEN คือ API token ของ useapi.net และ EMAIL คืออีเมลของบัญชี Google Flowที่เชื่อมไว้ สคริปต์ค้นหาบัญชีจากอีเมลให้เอง และตรวจฟิลด์ health ก่อนส่งงาน

อยาก clone ไปรันบนเครื่องตัวเอง? ไฟล์ 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();

ตัวอย่าง

วิดีโอ Veo 3.1 จริงที่สร้างผ่าน Google Flow API นี้ นำมาจากบันทึกการอัปเดตของเราโดยตรง

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 R2V แนวตั้งจากภาพอ้างอิงภาพเดียว

คำถามที่พบบ่อย

  • Veo 3.1 มี API ไหม? ผ่าน useapi.net มี Google Flow API รัน Veo 3.1 Quality, Fast และ Lite ผ่าน REST บนแพ็กเกจเว็บไซต์ Google Flow ของคุณ (Google AI Pro หรือ Ultra) วิดีโอ Veo 3.1 Quality ยาว 8 วินาทีราคา $0.80 บน Google AI Ultra $199.99 เทียบกับ $3.20 บน Veo API ทางการ (ข้อมูล ณ พฤศจิกายน 2026)
  • Veo 3.1 Quality, Fast และ Lite ต่างกันอย่างไร? ผ่าน useapi.net คุณเลือกโมเดลได้ทีละคำขอด้วยฟิลด์ model โดย veo-3.1-quality ให้คุณภาพสูงสุด veo-3.1-fast เป็นค่าเริ่มต้นที่สมดุลระหว่างความเร็วกับคุณภาพ และ veo-3.1-lite ถูกที่สุด คลิปละ $0.04 บน Google AI Ultra $199.99 เทียบกับ $0.40 บน Veo API ทางการ บนแพ็กเกจนั้น veo-3.1-lite-low-priority ราคา $0 ได้ราว 1,000 คลิปขึ้นไปต่อวัน (ข้อมูล ณ พฤศจิกายน 2026)
  • Veo สร้างวิดีโอจากภาพได้ไหม? ผ่าน useapi.net ได้ อัปโหลดเฟรมเริ่มด้วย POST /assets/email แล้วส่ง id อ้างอิงที่ได้ (ซ้อนอยู่ที่ mediaGenerationId.mediaGenerationId) เป็น startImage และจะใส่ endImage เพื่อกำหนดเฟรมสุดท้ายด้วยก็ได้ (ต้องมี startImage) ถ้าต้องการกำหนดตัวแบบหรือสไตล์แทนเฟรม ให้ส่ง referenceImage_N ได้สูงสุดสามภาพ ดูวิดีโอจากภาพด้านบน
  • ต้องแก้ captcha เองไหม? ผ่าน useapi.net คุณไม่ต้องแก้เองเลย Google Flow ขอ reCAPTCHA ทุกวิดีโอ และ worker ของ useapi.net แก้ให้อัตโนมัติ บัญชี Google Flow บัญชีแรกของคุณได้ฟรี 300 ครั้งบนคีย์ AntiCaptcha, CapSolver และ EzCaptcha ของเราเอง หลังจากนั้นคุณเพิ่มคีย์ผู้ให้บริการของคุณเองด้วย POST /accounts/captcha-providers สคริปต์ในคู่มือนี้ไม่ส่ง captchaToken
  • ต้องมี Veo 3.1 API key ไหม? ผ่าน useapi.net ไม่ต้องมี Google API key และไม่ต้องมีโปรเจกต์ Google Cloud ใช้ API token ของ useapi.net ตัวเดียวก็เรียก Veo 3.1 ได้ และเชื่อมบัญชี Google เพียงครั้งเดียวผ่านการตั้งค่าอัตโนมัติ ตัว Google Flow เองไม่มี API สาธารณะ และหลังจากโมเดล Veo 3.1 Preview บน Gemini API ปิดตัวเมื่อ 22 ตุลาคม 2026 Veo API ของ Google เองต้องใช้โปรเจกต์ Cloud ที่เปิดการเรียกเก็บเงิน

สรุป

ติดต่อเราได้ที่ Discord Server หรือ Telegram Channel สำหรับทุกคำถามและข้อสงสัยด้านการใช้งาน

ดูตัวอย่างโค้ดได้ใน GitHub repo ของเรา

เผยแพร่ที่อื่นด้วย

เผยแพร่บน dev.to ด้วย

เริ่มใช้งาน API

การสมัครสมาชิกเดือนละ $15 ใช้ได้กับทุก API บน useapi.net ใช้ Token เดียวกับทุกบริการ เชื่อมบัญชีของคุณเองได้สูงสุด 3 บัญชีต่อบริการ แต่ละบัญชีใช้แพ็กเกจของเว็บไซต์นั้นเอง ส่วน useapi.net ดูแล REST API การกระจายโหลด และการ polling ให้ ต้องการบัญชีมากกว่านี้? สมัครเพิ่มแต่ละครั้งเดือนละ $10 และได้เพิ่มอีก 3 บัญชีต่อบริการ

สมัครสมาชิก — $15/เดือน

ไม่มีบัตรเครดิตที่ใช้จ่ายต่างประเทศได้? สมัครด้วยคริปโตก็ได้ (การชำระด้วยคริปโตขอคืนเงินไม่ได้) ดูราคาและแพ็กเกจสำหรับปริมาณมากได้ที่หน้าการสมัครสมาชิก ถ้าชำระผ่าน Stripe ขอคืนเงินเต็มจำนวนได้ภายใน 14 วันหลังซื้อครั้งแรก หากสร้างงานสำเร็จไม่ถึง 50 ครั้ง สมัครไว้แล้ว? เปลี่ยนจำนวนได้ที่ Stripe Customer Portal โดยไม่ต้องซื้อใหม่