Turn a script into speech

November 4, 2026

Table of contents

  1. Request Headers
  2. Request Body
  3. Responses
  4. Model
  5. Examples
  6. Try It

Turn a script into speech with the Gemini voices of Google Vids. Each call returns one WAV file (24 kHz, mono) of the script read in one of the 30 voices listed by GET /voices.

Use it to hear how a voice reads your script before you make a talking avatar video, or to use the audio anywhere else.

Audio tags in square brackets, such as [whisper], [short pause] or [sarcasm], are performed, not read aloud:

Welcome back. [short pause] Today I have a secret for you. [whisper] Nobody else knows it yet.

Download the result with GET /media/mediaId. Like every Vids file it is temporary, so download it soon, see How long files last.

Cost

Each script uses 1 of the account’s monthly Gemini uses, shown as speech in GET /accounts/email.

Sync, async and webhooks

Speech usually takes a few seconds, so the default sync request answers with the finished job.

  • Every generation runs as a job on our side, with up to 5 minutes for Google’s answer, so a slow answer is never lost.
  • Sync (the default): the POST waits for the job and answers 200 with the finished job record, or the job’s error code. A video usually takes 20 to 100 seconds, a talking avatar video 1 to 2.5 minutes, an image about 10 and speech a few. If the job is still running after about 100 seconds, the POST answers 202 with the job as it stands: fetch the result with GET /jobs/jobid. Always check status: only completed has a result.
  • Async: pass async: true. The POST answers 202 at once with the job in status: "processing". Poll GET /jobs/jobid or wait for the webhook.
  • replyUrl receives one POST of the final job record, as JSON, when the job completes or fails, for sync and async requests alike. It does not make a request async.
  • Each account runs at most maxJobs jobs at a time (default 3, range 1 to 10, set with POST /accounts). GET /jobs shows what is running. Job records are kept for 7 days.

https://api.useapi.net/v1/google-vids/speech

Request Headers
Authorization: Bearer {API token}
Content-Type: application/json
# Alternatively you can use multipart/form-data
# Content-Type: multipart/form-data
Request Body
{
  "script": "Welcome back. [short pause] Today I have a secret for you. [whisper] Nobody else knows it yet.",
  "voice": "Kaci",
  "async": false,
  "replyUrl": "https://your-domain.com/webhook",
  "replyRef": "intro-voice-1"
}
  • script is required, the text to speak. Audio tags in square brackets are performed, not read.
    Length: 1 to 2500 characters.
  • voice is required, a voice name or voice id from GET /voices, case-insensitive.
  • email is optional, the account to run on. When omitted, a healthy account with a free maxJobs slot and speech left this month is used.
  • async is optional, true to answer 202 at once and run the job in the background. Default: false.
  • replyUrl is optional, a public http(s) URL that receives one POST of the job record when the job completes or fails. It works with sync and async requests.
    Callback body has the same JSON shape as GET /jobs/jobid response.
    Maximum length: 1024 characters.
  • replyRef is optional, your own reference echoed back in the job record.
    Maximum length: 1024 characters.
Responses
  • 200 OK — sync mode, the speech is ready.

    {
      "jobid": "j1104183512448203617s-u12345-email:[email protected]:google-vids",
      "type": "speech",
      "email": "[email protected]",
      "status": "completed",
      "created": "2026-11-04T18:35:12.448Z",
      "request": {
        "script": "Welcome back. [short pause] Today I have a secret for you. [whisper] Nobody else knows it yet.",
        "voice": "Kaci"
      },
      "updated": "2026-11-04T18:35:16.330Z",
      "completed": "2026-11-04T18:35:16.330Z",
      "result": {
        "mediaId": "user:[email protected]:eyJ1IjoiaHR0…",
        "duration": 6.12,
        "model": "/flix/generate_text_to_speech/v1",
        "quota": {
          "speech": {
            "limit": 50,
            "left": 33,
            "resetAt": "2026-12-01T08:00:00.000Z"
          }
        },
        "elapsedMs": 3882
      }
    }
    
  • 202 Accepted — the job is running: with async: true at once, in sync mode after about 100 seconds of waiting. Fetch the result with GET /jobs/jobid (or wait for the webhook).

    {
      "jobid": "j1104183702117390245s-u12345-email:[email protected]:google-vids",
      "type": "speech",
      "email": "[email protected]",
      "status": "processing",
      "created": "2026-11-04T18:37:02.117Z",
      "request": {
        "script": "Thanks for watching, see you next week.",
        "voice": "Holt",
        "async": true
      }
    }
    
  • 400 Bad Request — a parameter is missing or invalid, or the voice is unknown.

    {
      "error": "Unknown voice Bob. GET /voices lists them (name or voice id)",
      "code": 400
    }
    
    {
      "error": "Parameter script is required",
      "code": 400
    }
    
  • 401 Unauthorized

    Invalid API token.

    {
      "error": "useapi.net ⁝ Unauthorized",
      "code": 401
    }
    
  • 403 Forbidden — the account’s Google plan has no speech allowance, or the account has Google Workspace smart features turned off. The body is the failed job record with error.code: 403.

  • 404 Not Found — the account named by email is not connected.

    {
      "error": "Account [email protected] is not configured",
      "code": 404
    }
    
  • 422 Unprocessable Content — Google refused the script. Nothing was charged. The body is the failed job record:

    {
      "error": {
        "code": 422,
        "message": "Google refused this request (\"That request looks like it goes against our terms. Try asking something else.\"). Change the prompt or the inputs and try again."
      }
    }
    

    The body is the failed job record, shortened here to its error.

  • 429 Too Many Requests — the account is running maxJobs jobs, every account is busy, the month’s speech is used up, or Google is limiting the account for a while. When Google’s speech service itself is failing, it also answers this way: retry in a few minutes.

    {
      "error": "All 2 healthy accounts are running their maximum number of jobs (maxJobs), please retry shortly",
      "code": 429
    }
    

    Once Google refuses a job because the account’s monthly speech (Gemini) allowance is used up, the API keeps Google’s reset time on the account (exhaustedUntil in GET /accounts) and gives it no speech (Gemini) jobs until then. A request that must run on that account is answered at once with 429, with no job created and no call to Google. retryAt is when to try again. If the account’s plan changed, GET /accounts/email reads its allowance again and lets it take jobs right away.

    {
      "error": "Account [email protected] has used up this month's Google Vids speech (Gemini) allowance. It resets at 2026-11-01T07:00:00.000Z. If its plan changed, call GET /accounts/[email protected] to refresh it",
      "code": 429,
      "retryAt": "2026-11-01T07:00:00.000Z"
    }
    

    When nothing ties the request to one account, the job goes to an account with allowance left. When every healthy account has used up its speech (Gemini) allowance, retryAt is the earliest reset time:

    {
      "error": "All 3 healthy Google Vids accounts have used up this month's speech (Gemini) allowance. The first one resets at 2026-11-01T07:00:00.000Z. If a plan changed, call GET /accounts/{email} to refresh it",
      "code": 429,
      "retryAt": "2026-11-01T07:00:00.000Z"
    }
    

    A job that Google itself refuses for its allowance (the first refusal, or a job that needs more than the account has left) fails with error.code 429, error.retryAt set to the reset time, and error.quota with what was left (limit, left, resetAt). An account with some allowance left still takes smaller jobs.

  • 503 Service Unavailable — Google answered with a server error, or the async job could not be queued. Retry shortly. The body is the failed job record.

  • 596 Account Error

    Google reported the account as signed out. While we re-check it, the answer is the first message below. Retry in about a minute. If it stays signed out, the account is paused, you receive an email, and the answer becomes the second message until you re-add it via Setup Google Vids.

    {
      "error": "Google reported this account as signed out. We are re-checking it — please retry in about a minute. If it stays signed out, you will receive a re-add email.",
      "code": 596
    }
    
    {
      "error": "Account [email protected]: Google signed this account out. Re-add it at https://useapi.net/docs/start-here/setup-google-vids",
      "code": 596
    }
    

    Without email, the same code means no connected account is healthy and has a speech allowance:

    {
      "error": "No healthy Google Vids account with a speech (Gemini) allowance configured. Check GET /accounts",
      "code": 596
    }
    
Model

The job record, type: "speech". GET /jobs/jobid and the replyUrl webhook return the same shape. A speech result has a duration in seconds and no width, height, resolution or aspectRatio.

{ // TypeScript, all fields are optional
  jobid: string                  // j<time><random><v|i|s|a>-u<id>-email:<email>-bot:google-vids
  type: 'video' | 'image' | 'speech' | 'avatarVideo'
  mode?: 'text' | 'image' | 'ingredients' | 'extend' | 'upscale' | 'edit'   // video jobs only
  email: string                  // the account the job runs on
  status: 'processing' | 'completed' | 'failed'
  created: string                // ISO 8601, when the job was accepted
  updated?: string               // ISO 8601, the last status change
  completed?: string             // ISO 8601, when the job completed or failed
  request: Record<string, unknown>   // your request body as sent (ids as strings)
  replyUrl?: string
  replyRef?: string
  error?: {                      // status 'failed'
    code: number                 // 400 | 403 | 404 | 422 | 429 | 500 | 502 | 503 | 504 | 596, see GET /jobs/{jobid}
    message: string
    retryAt?: string             // code 429: ISO 8601, when to try again
    quota?: {                    // code 429 for allowance: Google's counter for this kind of job when it refused it
      limit: number              // monthly limit (seconds of video, images, Gemini uses or AI avatar videos)
      left: number               // what was left, 0 when used up
      resetAt: string            // ISO 8601, when the allowance resets
    }
  }
  result?: {                     // status 'completed'
    mediaId: string              // download it with GET /media/{mediaId}. A video: extend / upscale / edit it. An image: use it as startImage, referenceImage_N or avatar image. Speech: a WAV
    width?: number               // video and image
    height?: number              // video and image
    duration?: number            // video: length of the whole clip in whole seconds. Speech: length of the audio in seconds
    resolution?: '720p' | '1080p'              // video
    aspectRatio?: 'landscape' | 'portrait'     // video
    speechDuration?: number      // avatarVideo: length of the spoken script in seconds
    model: string | null         // Google's backend path, e.g. /flix/generate_videos_omni_t2v_psq/v1
    quota?: {                    // what the account has left after this job (absent when Google sends no counter)
      video?: { limit: number, left: number, resetAt: string }   // seconds, video jobs
      image?: { limit: number, left: number, resetAt: string }   // images, image jobs
      speech?: { limit: number, left: number, resetAt: string }  // Gemini uses, speech jobs; absent on plans Google does not meter (Ultra)
      avatarVideo?: { limit: number, left: number, resetAt: string }   // AI avatar videos, avatarVideo jobs
    }
    elapsedMs: number            // how long Google took
  }
}
Examples
  • curl -X POST "https://api.useapi.net/v1/google-vids/speech" \
       -H "Content-Type: application/json" \
       -H "Authorization: Bearer …" \
       -d '{
         "script": "Welcome back. [short pause] Today I have a secret for you. [whisper] Nobody else knows it yet.",
         "voice": "Kaci"
       }'
    
  • const token = "API token";
    const apiUrl = "https://api.useapi.net/v1/google-vids";
    const headers = { "Content-Type": "application/json", "Authorization": `Bearer ${token}` };
    
    const response = await fetch(`${apiUrl}/speech`, {
      method: "POST",
      headers,
      body: JSON.stringify({ script: "Welcome back. [short pause] Today I have a secret for you.", voice: "Kaci" })
    });
    const job = await response.json();
    if (job.status === "completed") {
      const audio = await fetch(`${apiUrl}/media/${encodeURIComponent(job.result.mediaId)}`, { headers });
      console.log("wav bytes", (await audio.arrayBuffer()).byteLength, "seconds", job.result.duration);
    } else
      console.log("response", response.status, job);
    
  • import requests
    from urllib.parse import quote
    
    token = "API token"
    apiUrl = "https://api.useapi.net/v1/google-vids"
    headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
    
    data = {"script": "Welcome back. [short pause] Today I have a secret for you.", "voice": "Kaci"}
    job = requests.post(f"{apiUrl}/speech", headers=headers, json=data).json()
    if job.get("status") == "completed":
        audio = requests.get(f"{apiUrl}/media/{quote(job['result']['mediaId'], safe='')}", headers=headers)
        with open("speech.wav", "wb") as f:
            f.write(audio.content)
    else:
        print(job)
    
Try It