---
title: "Gemini 3.1 Flash TTS"
description: "Text-to-speech on Google Gemini 3.1 Flash TTS, with 30 preset voices and 87 languages. It is best at a voice-over that you direct in plain words: tone, pace, accent, and emotion. It is the lowest per-character voice price in the catalog."
url: https://docs.roasos.com/models/audio/gemini-3.1-flash-tts
---

# Gemini 3.1 Flash TTS

<Callout>
  Family 

  `audio`

   · model 

  `gemini-3.1-flash-tts`

   · catalog 

  `model-catalog@2026-10-06.v5`

   · pricing 

  `customer-pricing@2026-11-05.v15`

  .
</Callout>

## Endpoint

```http
POST https://api.roasos.com/v1/audio/gemini-3.1-flash-tts
```

Send your API key as a bearer token. The key needs the scope `api:write`. Every paid request also needs an `Idempotency-Key` header.

The envelope fields `quote_only`, `quote_id`, `webhook_url` and `wait_timeout_seconds` behave the same on each model endpoint. Refer to [Jobs](/concepts/jobs) and [Quotes and credits](/concepts/quotes-and-credits).

## Good at

A voice-over or a hook line that must sound a specific way. Write the delivery in `style_instruction` ("Warm and unhurried, like a friend recommending something") and keep `text` for the spoken words only. Inline audio tags such as `[sigh]`, `[laughing]`, `[whispering]`, and `[short pause]` steer single moments. Two voices can speak in one take: send `speakers` with two `{ speaker_id, voice }` entries and start every line of `text` with the alias and a colon (`Host: ...`). `voice` takes one of the 30 preset names; `Kore` is the default. `language` takes a BCP-47 code such as `en-US` or `fr-FR`; omit it and the model detects the language. It does not clone a voice and does not accept media of any kind.

## Pricing

The unit is one **characters**. Pricing key: `gemini-3.1-flash-tts`.

| Request           | Credits |
| ----------------- | ------- |
| `1000_characters` | 1       |
| `2000_characters` | 2       |
| `5000_characters` | 3       |

The credits come from the roasOS pricing table. No other field changes the price. To get the exact price of your request, send it with `quote_only: true` first.

## Modes

* `text-to-speech`

Provider endpoints: `fal-ai/gemini-3.1-flash-tts`.

## Request fields

| Field                  | Type      | Required | Default | Rule                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| ---------------------- | --------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `language`             | enum      | no       | —       | `ar-EG`, `bn-BD`, `nl-NL`, `en-IN`, `en-US`, `fr-FR`, `de-DE`, `hi-IN`, `id-ID`, `it-IT`, `ja-JP`, `ko-KR`, `mr-IN`, `pl-PL`, `pt-BR`, `ro-RO`, `ru-RU`, `es-ES`, `ta-IN`, `te-IN`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `af-ZA`, `sq-AL`, `am-ET`, `ar-001`, `hy-AM`, `az-AZ`, `eu-ES`, `be-BY`, `bg-BG`, `my-MM`, `ca-ES`, `ceb-PH`, `cmn-CN`, `cmn-TW`, `hr-HR`, `cs-CZ`, `da-DK`, `en-AU`, `en-GB`, `et-EE`, `fil-PH`, `fi-FI`, `fr-CA`, `gl-ES`, `ka-GE`, `el-GR`, `gu-IN`, `ht-HT`, `he-IL`, `hu-HU`, `is-IS`, `jv-JV`, `kn-IN`, `kok-IN`, `lo-LA`, `la-VA`, `lv-LV`, `lt-LT`, `lb-LU`, `mk-MK`, `mai-IN`, `mg-MG`, `ms-MY`, `ml-IN`, `mn-MN`, `ne-NP`, `nb-NO`, `nn-NO`, `or-IN`, `ps-AF`, `fa-IR`, `pt-PT`, `pa-IN`, `sr-RS`, `sd-IN`, `si-LK`, `sk-SK`, `sl-SI`, `es-419`, `es-MX`, `sw-KE`, `sv-SE`, `ur-PK` |
| `output_format`        | enum      | no       | `mp3`   | `mp3`, `wav`, `ogg_opus`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `quote_id`             | string    | no       | —       | UUID                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `quote_only`           | boolean   | no       | `false` | —                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `speakers`             | object\[] | no       | —       | 1–2 items; each `{ speaker_id, voice }`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `style_instruction`    | string    | no       | —       | ≥ 1 character; ≤ 2,000 characters                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `temperature`          | number    | no       | `1`     | 0–2                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `text`                 | string    | yes      | —       | ≥ 1 character; ≤ 5,000 characters                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `voice`                | enum      | no       | `Kore`  | `Kore`, `Puck`, `Charon`, `Zephyr`, `Aoede`, `Fenrir`, `Leda`, `Orus`, `Callirrhoe`, `Autonoe`, `Enceladus`, `Iapetus`, `Umbriel`, `Algieba`, `Despina`, `Erinome`, `Algenib`, `Rasalgethi`, `Laomedeia`, `Achernar`, `Alnilam`, `Schedar`, `Gacrux`, `Pulcherrima`, `Achird`, `Zubenelgenubi`, `Vindemiatrix`, `Sadachbia`, `Sadaltager`, `Sulafat`                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `wait_timeout_seconds` | integer   | no       | `0`     | 0–60                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `webhook_url`          | string    | no       | —       | https URL; ≤ 4,096 characters                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |

The schema is strict. A field that is not in this table gets `400 unsupported_option`. The field names are the roasOS role names. One role has one name in every family.

## Media

This endpoint accepts no media.

Every other role is refused: `avatar_id`, `elements`, `product_id`, `product_images`, `references`, `reference_images`, `image_url`, `end_image_url`, `scene_id`, `scene_image`, `video_urls`, `video_url`, `audio_urls`, `audio_url`, `voice_id`. A refused role gets `400 unsupported_option`.

<Callout>
  Each media URL must be an immutable object on 

  `https://cdn.roasos.com`

   that your workspace owns. Any other URL gets 

  `400 reference_media_not_workspace_owned`

  . Refer to 

  [Reference media and uploads](/concepts/reference-media)

   to learn how to put a file there.
</Callout>

## Prompt

`text` limit 5,000 characters; `style_instruction` limit 2,000. roasOS adds no wrapper.

* Put the delivery in `style_instruction`, not in `text`. The model speaks every word of `text`.
* Use one audio tag right before the words it changes. Too many tags make the read unstable.
* For two voices, make each alias match a `speaker_id` exactly, and start each line with it.
* Set `language` when the text mixes languages or when the accent matters.
* Choose the voice by its character (Kore firm, Puck upbeat, Charon informative, Zephyr bright, Aoede breezy), then let the direction do the rest.
* Lower `temperature` for a steady, repeatable read; raise it for a more varied one.

## Not exposed

These catalog entries describe omitted inputs, pinned values and source constraints. They do not imply that the provider accepts every listed field.

* `voice_id` — pinned to `the model has no provider voice-clone registry`.
* `references` — not accepted by this public endpoint.
* `avatar_id` — not accepted by this public endpoint.
* `product_id` — not accepted by this public endpoint.
* `audio_urls` — not accepted by this public endpoint.
* `image_url` — not accepted by this public endpoint.
* `speed` — not accepted by this public endpoint.
* `pitch` — not accepted by this public endpoint.
* `emotion` — not accepted by this public endpoint.

## Result

The result kind is `audio`. Always present: `url`. Present when the run makes it: `timestamps`.

```json
{
  "data": {
    "id": "gen_…",
    "kind": "audio",
    "model": "gemini-3.1-flash-tts",
    "status": "completed",
    "result": {
      "timestamps": [
        {}
      ],
      "url": "https://cdn.roasos.com/workspaces/ws_01/result"
    }
  }
}
```

The files are immutable objects on `https://cdn.roasos.com`. A result URL is a valid media input for your next request.

## Example

The three steps are always the same: quote, run, poll.

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="JavaScript">
      JavaScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash
    # 1. Quote. Nothing is spent.
    curl -X POST https://api.roasos.com/v1/audio/gemini-3.1-flash-tts \
      -H "Authorization: Bearer roasos_sk_live_..." \
      -H "Idempotency-Key: 0f1c9e2a-gemini-3.1-flash-tts-0001" \
      -H "Content-Type: application/json" \
      -d '{"quote_only":true,"text":"Two drops, morning and night. That is the whole routine."}'
    # 200 { "data": { "kind": "quote", "quote_id": "q_7Hk...", "credits": 1, "expires_at": "...",
    #        "confirmation": { "model": "gemini-3.1-flash-tts", "example": "1000_characters" } } }

    # 2. Run. Same body, same Idempotency-Key, quote_id in place of quote_only.
    curl -X POST https://api.roasos.com/v1/audio/gemini-3.1-flash-tts \
      -H "Authorization: Bearer roasos_sk_live_..." \
      -H "Idempotency-Key: 0f1c9e2a-gemini-3.1-flash-tts-0001" \
      -H "Content-Type: application/json" \
      -d '{"quote_id":"q_7Hk...","text":"Two drops, morning and night. That is the whole routine."}'
    # 201 { "data": { "id": "gen_...", "status": "queued", "credits_reserved": 1 } }

    # 3. Poll, then read the result.
    curl https://api.roasos.com/v1/jobs/gen_.../status -H "Authorization: Bearer roasos_sk_live_..."
    curl https://api.roasos.com/v1/jobs/gen_.../result -H "Authorization: Bearer roasos_sk_live_..."
    ```
  </CodeBlockTab>

  <CodeBlockTab value="JavaScript">
    ```javascript
    const key = process.env.ROASOS_API_KEY;
    const url = "https://api.roasos.com/v1/audio/gemini-3.1-flash-tts";
    const idempotencyKey = "0f1c9e2a-gemini-3.1-flash-tts-0001";
    const input = {
      "text": "Two drops, morning and night. That is the whole routine."
    };

    const post = (payload) =>
      fetch(url, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${key}`,
          "Idempotency-Key": idempotencyKey,
          "Content-Type": "application/json",
        },
        body: JSON.stringify(payload),
      }).then((response) => response.json());

    // 1. Quote. Nothing is spent.
    const quote = await post({ quote_only: true, ...input });
    console.log(quote.data.credits);

    // 2. Run.
    const job = await post({ quote_id: quote.data.quote_id, ...input });

    // 3. Poll, then read the result.
    let status = "queued";
    while (status !== "completed" && status !== "failed" && status !== "canceled") {
      await new Promise((done) => setTimeout(done, 3000));
      const poll = await fetch(`${url.split("/v1")[0]}/v1/jobs/${job.data.id}/status`, {
        headers: { Authorization: `Bearer ${key}` },
      }).then((response) => response.json());
      status = poll.data.status;
    }

    const result = await fetch(`${url.split("/v1")[0]}/v1/jobs/${job.data.id}/result`, {
      headers: { Authorization: `Bearer ${key}` },
    }).then((response) => response.json());
    console.log(result.data.result);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import os, time, requests

    key = os.environ["ROASOS_API_KEY"]
    url = "https://api.roasos.com/v1/audio/gemini-3.1-flash-tts"
    headers = {
        "Authorization": f"Bearer {key}",
        "Idempotency-Key": "0f1c9e2a-gemini-3.1-flash-tts-0001",
    }
    payload = {
        "text": "Two drops, morning and night. That is the whole routine."
    }

    # 1. Quote. Nothing is spent.
    quote = requests.post(url, headers=headers, json={"quote_only": True, **payload}).json()
    print(quote["data"]["credits"])

    # 2. Run.
    job = requests.post(url, headers=headers, json={"quote_id": quote["data"]["quote_id"], **payload}).json()
    job_id = job["data"]["id"]

    # 3. Poll, then read the result.
    jobs = "https://api.roasos.com/v1/jobs"
    status = "queued"
    while status not in ("completed", "failed", "canceled"):
        time.sleep(3)
        status = requests.get(f"{jobs}/{job_id}/status", headers=headers).json()["data"]["status"]

    result = requests.get(f"{jobs}/{job_id}/result", headers=headers).json()
    print(result["data"]["result"])
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## The deprecated path

`POST /v1/generations` still reaches this model for one minor version. It is deprecated. This is how it maps:

`POST /v1/audio/gemini-3.1-flash-tts` is the endpoint. `POST /v1/generations` also works with `{ "type": "audio", "model": "gemini-3.1-flash-tts", "prompt": "<text>", "options": { "voice": "Puck", "style_instruction": "Warm and amused.", "language": "en-GB" } }`; there the script goes in `prompt`. `options.speed`, `pitch`, `stability`, and `similarity_boost` have no provider field and are accepted at their default values only.

## See also

* [`llms.txt` for this model](https://docs.roasos.com/models/audio/gemini-3.1-flash-tts/llms.txt) — the same facts as one plain-text file for an agent.
* `GET https://api.roasos.com/v1/catalog/gemini-3.1-flash-tts` — the machine-readable form: enums, defaults, limits and prices. Refer to the [API reference](/api-reference/overview).
* [Jobs](/concepts/jobs) — how to poll a job and read its result.
* Provider endpoints: `fal-ai/gemini-3.1-flash-tts`.
