# DubVoice.ai Public API

Base URL: `https://www.dubvoice.ai/api/v1` (TTS / voices / translate / video / me)
Other endpoints: `https://www.dubvoice.ai/api/<feature>` (image, music, edge-tts, minimax-tts, kokoro-tts).

All endpoints accept either of:
- `Authorization: Bearer sk_your_api_key`
- `X-API-Key: sk_your_api_key`

Generate / rotate keys at https://www.dubvoice.ai/dashboard/api-docs.

---

## tab:elevenlabs — ElevenLabs TTS

### POST /api/v1/tts
Submit a text-to-speech job. Returns a `task_id` immediately. Poll the job
with `GET /api/v1/tts?task_id=...`. Cost = 1 credit per character.

**Request body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | yes | Text to synthesise. Up to 100,000 characters. |
| `voice_id` | string | yes | A 15-25 char alphanumeric ElevenLabs voice id (e.g. `21m00Tcm4TlvDq8ikWAM`), **or** a prefixed `vbee_...` / `fishaudio_...` id from `GET /voices?provider=vbee|fishaudio` (see the *Vbee & Fish Audio* tab). List with `GET /voices`. Minimax ids are rejected here — use `POST /api/minimax-tts`. |
| `model_id` | string | no | `eleven_multilingual_v2` (default — only live model right now). `eleven_turbo_v2_5` / `eleven_flash_v2_5` / `eleven_v3` are temporarily under maintenance and will return HTTP 503. |
| `language` | string | no | `auto` (default — server auto-detects from `text`) or ISO 639-1 code (`en`, `tr`, `es`, `fr`, `de`, `it`, `pt`, `ru`, `pl`, `cs`, `ro`, `uk`, `nl`, `ar`, `hi`, `ja`, `ko`, `zh`, ...). Invalid values are dropped silently and the server falls back to auto-detect. |
| `voice_settings` | object | no | See below. |
| `webhook_url` | string | no | Called once with the final audio URL when the job finishes. |

**voice_settings object**

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `speed` | number | 1.0 | 0.5 – 1.5. Out-of-range values fall back to the default. |
| `context_chaining` | boolean | false | Keeps multi-turn voice continuity across the whole script. **Bills +50% credits.** |
| `with_transcript` | boolean | false | Asks the upstream for an SRT subtitle file alongside the audio. |
| `pronunciation_dictionary_id` | string | – | Applies the given dictionary's rules to the text before synthesis. Only affects audio. |
| `stability` | number | 0.5 | **Ignored** — kept for backward compat. |
| `similarity_boost` | number | 0.75 | **Ignored** — kept for backward compat. |
| `style` | number | 0 | **Ignored** — kept for backward compat. |
| `use_speaker_boost` | boolean | true | **Ignored** — kept for backward compat. |

The performance controls (stability / similarity / style / speaker boost) no longer affect the upstream — performance is now driven by **inline tags in `text`** (see *Performance tags* below). Sending the old fields is harmless; they're silently dropped.

**Performance tags (inline in `text`)**

Drop these tags directly into the text — they steer pacing, pauses, emotion, sound effects, style and tone.

- **Speed:** `[speed_very_slow]` · `[speed_slow]` · `[speed_fast]` · `[speed_very_fast]`
- **Pause:** `[pause]` · `[long_pause]`
- **Emotion (31):** `[affection]`, `[amusement]`, `[anger]`, `[awe]`, `[confusion]`, `[contentment]`, `[curiosity]`, `[disappointment]`, `[doubt]`, `[empathy]`, `[envy]`, `[excitement]`, `[fear]`, `[guilt]`, `[happiness]`, `[hesitation]`, `[hope]`, `[hurt]`, `[interest]`, `[joy]`, `[love]`, `[nostalgia]`, `[pity]`, `[pride]`, `[relief]`, `[sadness]`, `[satisfaction]`, `[shame]`, `[surprise]`, `[sympathy]`, `[wonder]`
- **Sound effects (onomatopoeia is part of the tag — keep it attached):** `[cough]ahem` · `[laughter]haha` · `[crying]boohoo` · `[screaming]ahh` · `[burping]burp` · `[humming]hmm` · `[sigh]uh` · `[sniff]sniff` · `[sneeze]achoo`
- **Style:** `[whispering]` · `[shouting]` · `[singing]` · `[narrating]` · `[storytelling]` · `[announcing]` · `[reporting]` · `[conversational]`
- **Tone:** `[serious]` · `[playful]` · `[formal]` · `[casual]` · `[confident]` · `[warm]` · `[friendly]` · `[calm]` · `[authoritative]` · `[sarcastic]` · `[dramatic]` · `[mysterious]`

Tags go between words or between sentences — never inside a word. The dashboard exposes a one-click tag picker plus an *Enhance text with AI* button that injects them automatically (via `POST /api/tts/enhance`).

**Example**

```bash
curl -X POST "https://www.dubvoice.ai/api/v1/tts" \
  -H "Authorization: Bearer sk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "[warm] Hello there. [pause] [excitement] Today is going to be amazing!",
    "voice_id": "21m00Tcm4TlvDq8ikWAM",
    "model_id": "eleven_multilingual_v2",
    "language": "auto",
    "voice_settings": { "speed": 1.0, "context_chaining": false }
  }'
```

**Response (immediate)**

```json
{ "task_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "characters": 30 }
```

### GET /api/v1/tts?task_id=&lt;id&gt;
Poll a single job. Returns status, audio URL, progress %, and error if any.

```json
{
  "task_id": "...", "status": "completed",
  "result": "https://your-storage.supabase.co/.../final.mp3",
  "characters": 30, "progress": 100,
  "created_at": "...", "completed_at": "..."
}
```

Statuses: `pending` → `processing` → `completed` | `failed`. Recommended poll
interval: 3-5 seconds.

### GET /api/v1/tts
Paginated list of your jobs: `?page=1&limit=20` (max 100).

### Limits
- 5 parallel jobs per user.
- 20 requests / minute per API key.
- Max text length: 100,000 chars per job.

---

## tab:edge — Edge TTS

Free Microsoft Edge voices. No credits consumed.

### POST /api/edge-tts
Returns the MP3 binary directly (no polling).

**Request body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | yes | Text to synthesise. |
| `voice` | string | yes | Edge voice short name (e.g. `en-US-AriaNeural`, `tr-TR-EmelNeural`). |
| `rate` | string | no | `+10%`, `-20%` etc. |
| `pitch` | string | no | `+5Hz`, `-10Hz` etc. |

```bash
curl -X POST "https://www.dubvoice.ai/api/edge-tts" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "text": "Hello world", "voice": "en-US-AriaNeural" }' \
  --output out.mp3
```

### GET /api/edge-tts
Returns the full Edge voice catalogue (60+ voices, 30+ languages):

```json
{ "voices": [{ "ShortName": "en-US-AriaNeural", "Gender": "Female", "Locale": "en-US" }, ...] }
```

---

## tab:kokoro — Kokoro TTS

Hexgrad's open-source Kokoro-82M model on our own voice-studio
backend (self-hosted Kokoro-FastAPI container). **No rate limit, no
text-length cap** — Kokoro auto-chunks long input server-side.
**70 % discount on the ElevenLabs tier — 1 character = 0.3 credits** (~3.33 chars = 1 credit).

### POST /api/kokoro-tts
Submits the job, polls until done (typically 3-30 s for short text,
up to ~5 min for very long text), then returns the WAV binary directly.
On failure the route refunds credits and returns `{ status: "failed", job_id, message }`
with HTTP 202 / 500 so the caller retries.

**Request body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | yes | Text to synthesise — no length cap, Kokoro chunks internally. Legacy alias: `script`. |
| `voice` | string | yes | Kokoro voice id (e.g. `af_heart`, `am_michael`, `bf_emma`, `jf_alpha`, `zm_yunjian`) OR a weighted mix like `af_bella+af_sky(2)`. Legacy aliases: `voice_id`, `preset_id`. List with `GET /api/kokoro-tts`. |

```bash
# Single voice
curl -X POST "https://www.dubvoice.ai/api/kokoro-tts" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "text": "Hello world from Kokoro!", "voice": "af_heart" }' \
  --output out.wav

# Voice mix — 1 part af_bella + 2 parts af_sky
curl -X POST "https://www.dubvoice.ai/api/kokoro-tts" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "text": "Mixed voice sample", "voice": "af_bella+af_sky(2)" }' \
  --output mix.wav
```

### GET /api/kokoro-tts
Returns the full Kokoro voice catalog (54 voices across English (US/UK),
Spanish, French, Hindi, Italian, Japanese, Brazilian Portuguese, Mandarin
Chinese), voice-mixing support flag, and the pricing tier:

```json
{
  "model": "kokoro_tts",
  "voices": [{ "id": "af_heart", "name": "Heart", "language": "English (US)", "locale": "en-US", "gender": "female" }, ...],
  "supports_voice_mixing": true,
  "mixing_syntax": "voice1+voice2 or voice1+voice2(N) for weighted mixes",
  "pricing": { "credits_per_char": 0.3, "note": "70% discount vs ElevenLabs (0.3 credits per character)." }
}
```

Response headers on a successful synth: `X-Credits-Used`, `X-Characters`,
`X-Credits-Remaining`, `X-Job-Id`.

---

## tab:vbee-fishaudio — Vbee & Fish Audio TTS

Two extra voice libraries that synthesise through the **same endpoint and
job flow as ElevenLabs** — `POST /api/v1/tts`. There is no separate
synthesis endpoint: list the voices, then pass the returned `voice_id`
straight to `/api/v1/tts`. **1 character = 1 credit**, same as ElevenLabs.

| Provider | Voices | Focus |
|----------|--------|-------|
| `vbee` | **1,649** | Vietnamese-heavy (1,212 of them) but 52 languages in total — Arabic, Hindi, Japanese, French, Indonesian, Filipino … |
| `fishaudio` | **275** | Multilingual community library, 15 languages (96 English, plus Chinese, Spanish, Portuguese, Arabic, Turkish, …) |

### 1. List the voices

```bash
curl "https://www.dubvoice.ai/api/v1/voices?provider=vbee&page_size=50" \
  -H "Authorization: Bearer sk_..."

curl "https://www.dubvoice.ai/api/v1/voices?provider=fishaudio&gender=female" \
  -H "Authorization: Bearer sk_..."
```

Both providers ship as bundled catalogues, so listing needs no upstream
round-trip. Filters `search`, `language`, `gender`, `page`, `page_size`
work the same as for ElevenLabs. Each voice comes back with its id
**already prefixed**:

```json
{
  "voices": [
    {
      "voice_id": "vbee_n_phutho_female_anbinhan_advertise_vc",
      "name": "An Bình An",
      "gender": "female",
      "language": "Vietnamese",
      "preview_url": "https://..."
    }
  ],
  "total": 1649, "page": 1, "page_size": 50, "provider": "vbee"
}
```

> **`language` filter — the two providers label languages differently.**
> `vbee` uses full names (`Vietnamese`, `Arabic`, `Bengali (India)`),
> `fishaudio` uses ISO 639-1 codes (`en`, `zh`, `es`, `tr`). The filter is
> an exact, case-insensitive match, so `?provider=fishaudio&language=english`
> returns nothing — use `language=en`. When in doubt, list without the
> filter and read the `language` field.

### 2. Synthesise

Pass the prefixed id to the normal TTS endpoint — do **not** strip the
`vbee_` / `fishaudio_` prefix, it is what selects the provider upstream.

```bash
# Vbee (Vietnamese)
curl -X POST "https://www.dubvoice.ai/api/v1/tts" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Xin chào, đây là giọng đọc tiếng Việt.",
    "voice_id": "vbee_n_phutho_female_anbinhan_advertise_vc",
    "voice_settings": { "speed": 1.0 }
  }'

# Fish Audio (English)
curl -X POST "https://www.dubvoice.ai/api/v1/tts" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Hello, this is a Fish Audio voice.",
    "voice_id": "fishaudio_7ffbc30676794fd989cda022638314b5"
  }'
```

Returns a `task_id` immediately; poll `GET /api/v1/tts?task_id=...` exactly
as with ElevenLabs. Statuses, webhooks, concurrency caps and the 100,000
character limit are all identical.

`model_id` is ignored for these providers — the upstream picks the model
from the voice prefix. You may send it or omit it.

**Notes**

- Inline performance tags (`[warm]`, `[pause]`, …) are an ElevenLabs
  feature and are not interpreted by Vbee / Fish Audio — the text is read
  as written, so leave the tags out.
- `speed` (0.5–1.5) is honoured. The other legacy `voice_settings` fields
  are ignored, same as on the ElevenLabs path.
- An ElevenLabs maintenance window does **not** block these providers.
- Sending a Minimax voice id here returns `MINIMAX_NOT_SUPPORTED_HERE` —
  use `POST /api/minimax-tts` for those.

---

## tab:minimax — Minimax TTS

High-quality multilingual TTS using Minimax models. 1 character = 1 credit.
**441 voices** across 30+ languages, system voices and clones alike.

### POST /api/minimax-tts
Synchronous — returns the audio URL when done (~5-30 s).

**Request body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | yes | Up to 5,000 chars per call. |
| `voice_id` | string | yes | Full Minimax `voice_id` from `POST /api/minimax-tts/voices`. Usually `<Language>_<Name>` (e.g. `English_CalmWoman`, `Russian_AttractiveGuy`); some newer voices use a numeric id (e.g. `362703657091275`). Pass whichever `voice_id` the list returned, verbatim. **Do not** send the short `voice_name` (e.g. `CalmWoman`) — Minimax rejects it with HTTP 500. |
| `model` | string | no | `speech-2.6-hd` (default), plus `speech-2.8-hd`, `speech-2.8-turbo`, `speech-2.6-turbo`, `speech-2.5-hd-preview`, `speech-2.5-turbo-preview`, `speech-02-hd`, `speech-02-turbo`, `speech-01-hd`, `speech-01-turbo`. All variants cost 1 credit per character. |
| `language_boost` | string | no | `auto` (default), `English`, `Turkish`, `Spanish`, etc. |
| `speed` | number | no | 0.5–2.0 (default 1.0). |
| `vol` | number | no | 0–10 (default 1.0). |
| `pitch` | number | no | -12 to 12 (default 0). |
| `emotion` | string | no | `happy`, `sad`, `angry`, `fearful`, `disgusted`, `surprised`, `neutral`. |

```bash
curl -X POST "https://www.dubvoice.ai/api/minimax-tts" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Merhaba dünya",
    "voice_id": "English_CalmWoman",
    "language_boost": "auto"
  }'
```

### POST /api/minimax-tts/voices
List Minimax voices (441 total). Empty body `{}` returns the first 100.
Filter with `tag_list`: `["Turkish"]`, `["Male", "Mature"]`, `["Clone"]`.

Each voice in the response has both `voice_id` (long, e.g. `English_CalmWoman`,
or numeric for newer voices) and `voice_name` (short display label, e.g.
`Abbess`). **Always pass the `voice_id` to `/api/minimax-tts`** — the short
`voice_name` is for display only and Minimax rejects it with HTTP 500.

`sample_audio` now carries a playable preview URL for 427 of the 441 voices.
Where it is still an empty string no precomputed preview exists upstream —
synthesise one on demand by sending a few words through `/api/minimax-tts`
with the desired `voice_id`.

```bash
curl -X POST "https://www.dubvoice.ai/api/minimax-tts/voices" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "tag_list": ["Turkish"] }'
```

### POST /api/voice-clone — Clone a voice
Free. Uploads a sample audio file and returns a `cloned_voice_id`
that can be passed to `/api/minimax-tts` like any other voice.
Synthesis with the clone bills the standard 1 character = 1 credit.

**Multipart body**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `file` | file | yes | mp3 / m4a / wav / ogg / flac. 10 s – 5 min, max 10 MB. |
| `voice_name` | string | no | Display name (default `"My Voice"`). |
| `language_tag` | string | no | Primary language label (English / Turkish / Spanish / …). Default `English`. |
| `gender_tag` | string | no | `male` / `female` (default `male`). |

```bash
curl -X POST "https://www.dubvoice.ai/api/voice-clone" \
  -H "Authorization: Bearer sk_..." \
  -F "file=@sample.mp3" \
  -F "voice_name=Rachel Clone" \
  -F "language_tag=English" \
  -F "gender_tag=female"
```

→ `{ "success": true, "cloned_voice_id": "user_42_voice_1738123456789", "voice_name": "Rachel Clone" }`

Then synthesise with it:

```bash
curl -X POST "https://www.dubvoice.ai/api/minimax-tts" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "text": "Hello", "voice_id": "user_42_voice_1738123456789" }'
```

### GET /api/voice-clone — List cloned voices
```bash
curl "https://www.dubvoice.ai/api/voice-clone" \
  -H "Authorization: Bearer sk_..."
```

### DELETE /api/voice-clone?voice_id=&lt;id&gt;
```bash
curl -X DELETE "https://www.dubvoice.ai/api/voice-clone?voice_id=user_42_voice_1738123456789" \
  -H "Authorization: Bearer sk_..."
```

---


## tab:audio — Audio Tools (STT, Voice Changer, Dubbing, Dialogue)

These `/api/v1/*` endpoints accept JSON with a public `audio_url` / `source_url`.
(The non-v1 paths — /api/speech-to-text, /api/voice-changer, /api/dubbing —
still accept a direct multipart/form-data file upload.)
Auth: `Authorization: Bearer sk_...` or `X-API-Key`. Credits are charged per use and refunded on failure.

### POST /api/v1/stt — Speech to Text
Transcribe audio to text (returns JSON + SRT transcript URLs). Source ≤200MB. 1,000 credits/minute.
```bash
curl -X POST "https://www.dubvoice.ai/api/v1/stt" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "audio_url": "https://example.com/audio.mp3" }'
```

### POST /api/v1/voice-changer — Voice Changer
Transform the voice in an audio file into a target voice. 2,000 credits/minute.
```bash
curl -X POST "https://www.dubvoice.ai/api/v1/voice-changer" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "audio_url": "https://example.com/source.mp3", "target_voice_id": "21m00Tcm4TlvDq8ikWAM" }'
```

### POST /api/v1/dubbing — Auto Dubbing
Dub audio into a target language (returns dubbed audio + SRT). Source: MP3/M4A ≤20MB or 5 min. 30,000 credits per task.
```bash
curl -X POST "https://www.dubvoice.ai/api/v1/dubbing" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "source_url": "https://example.com/speech.mp3", "target_lang": "tr" }'
```

### POST /api/v1/dialogue — Text to Dialogue
Render a multi-speaker dialogue: each segment is synthesized with its own voice and concatenated.
Provider `minimax` (default) or `edge`. Credits charged per segment.
```bash
curl -X POST "https://www.dubvoice.ai/api/v1/dialogue" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "provider": "minimax", "segments": [
    { "voice_id": "English_CalmWoman", "text": "Hello, how are you?" },
    { "voice_id": "English_Gentleman", "text": "I am doing great, thanks!" }
  ] }'
```

---

## tab:translate — Translate

1 credit per 10 characters of input text (Math.ceil — e.g. 250 chars = 25 cr). Cultural-adapted translation. Credits refunded if every chunk fails.

### POST /api/v1/translate

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | yes | Up to 100,000 chars. |
| `target_language` | string | yes | English name (`Turkish`, `Spanish`, ...) or Turkish name (`Türkçe`, `İspanyolca`). |
| `source_language` | string | no | `auto` / `Auto Detect` (default `English`). |

```bash
curl -X POST "https://www.dubvoice.ai/api/v1/translate" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "text": "Merhaba, bugün nasılsın?", "target_language": "English", "source_language": "auto" }'
```

```json
{
  "success": true,
  "translated_text": "Hello, how are you today?",
  "source_language": "Turkish",
  "detected_language": "Turkish",
  "target_language": "English",
  "chunks_processed": 1,
  "credits_used": 3,
  "remaining_credits": 249997
}
```

### GET /api/v1/translate
Returns the supported language list.

---

## tab:video — Video Generation

All video endpoints share the same auth (sk_…). Each provider charges credits up-front and refunds on failure.

### POST /api/v1/video — Veo / Meta AI / Omni Flash / Kling
Single endpoint that fans out to multiple providers based on `model`. All
durations are inclusive of audio; failures are refunded automatically.

**Veo 3.1 family** (Google, 60-120 s, 8 s fixed clip):

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `prompt` | string | yes | English, 10-2000 chars. |
| `model` | string | no | `veo-3.1-fast` (default, **7,500 cr**) / `veo-3.1-lite` (13,000 cr, synced audio) / `veo-3.1` (17,000 cr, HQ). |
| `resolution` | string | no | `720p` (default) / `1080p` (~1.5× cr). |
| `aspect_ratio` | string | no | `16:9` (default). **`veo-3.1-lite` also accepts `9:16`** (portrait, for Reels/Shorts). `veo-3.1` / `veo-3.1-fast` are 16:9 only. |
| `duration` | number | no | Fixed at 8s for the Veo 3.1 family — any value is ignored. |
| `image_base64` | string | no | Legacy single-image alias for `ref_images[0]` (base64 data URI). |
| `ref_images` | string[] | no | Reference images as base64 data URIs (or http(s) URLs). `frame` mode: up to 2 (ordered `[start, end]`). `ingredient` mode: up to 3 on `veo-3.1` / `veo-3.1-fast`, up to **4** on `veo-3.1-lite`. |
| `mode_image` | string | no | `frame` (default) — first image = start frame, second = end frame. `ingredient` — subject/style references. Supported across all Veo 3.1 variants. |

**Omni Flash** (`model: "omniflash"`, 16:9 / 9:16). Duration-tiered, text-to-video only:

| Duration | Credits |
|----------|--------:|
| 4 s  | 4,688 |
| 6 s  | 6,250 |
| 8 s (default) | 7,813 |
| 10 s | 9,375 |

Fields: `prompt`, `model: "omniflash"`, `aspect_ratio` (`16:9` / `9:16`), `duration` (4 / 6 / 8 / 10), `ref_images` (optional, **up to 7** base64 data URIs or http(s) URLs used as visual context).

**Meta AI** (`model: "meta"`) — **flat 2,000 credits** per clip, the cheapest video model here.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `prompt` | string | yes | English scene description. |
| `model` | string | yes | `meta` |
| `aspect_ratio` | string | no | `16:9` (default) / `9:16` / `1:1`. |
| `resolution` | string | no | `720p` (default) / `480p`. |
| `start_frame` | string | no | Base64 data URI or http(s) URL. Supplying it switches Meta to image-to-video. |
| `end_frame` | string | no | Optional second frame — Meta interpolates from start to end. |

`duration` is ignored: Meta returns its own clip length, which is why it prices flat. Meta does **not** use `ref_images` — pass `start_frame` / `end_frame` instead (`ref_images[0]` and `[1]` are accepted as a fallback so callers reusing the Omni Flash shape still work).

```bash
curl -X POST "https://www.dubvoice.ai/api/v1/video" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "waves crashing on a rocky shore at dawn, cinematic",
    "model": "meta",
    "aspect_ratio": "9:16",
    "resolution": "720p"
  }'
```

**Kling** (`model: "kling-video-2-5"` / `"kling-video-3-0"`). Mode × model pricing per 5 s clip — at 10 s, double the cost:

|                 | standard (720p) | professional (1080p) |
|-----------------|----------------:|---------------------:|
| Kling 2.5       | 75,000          | 100,000              |
| Kling 3.0       | 125,000         | 150,000              |

Fields: `prompt`, `model`, `mode` (`standard` / `professional`), `aspect_ratio` (`16:9` / `9:16` / `1:1`), `duration` (3-15 s, default 5).

### POST /api/video/grok — Grok Imagine
xAI Grok video. Renders synchronously (~60-120 s) and returns the final
`file_url`; refunds credits automatically on failure.

| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `prompt` | string | — | English, 10-5000 chars |
| `duration` | number | 6 | **6 or 10 seconds only** — other values snap to the nearest allowed. |
| `resolution` | string | `480p` | `480p` / `720p` |
| `aspect_ratio` | string | `16:9` | `16:9`, `9:16`, `1:1`, `2:3`, `3:2` |
| `image_base64` | string | — | `data:image/...` URI — auto-switches to image-to-video. |
| `image_url` | string | — | Public http(s) URL — same effect as `image_base64`. |

Cost — flat-rate matrix (per clip, not per second):

| Duration | 480p | 720p |
|---|---|---|
| 6 s  | **3,000 cr** | **4,000 cr** |
| 10 s | **5,000 cr** | **5,500 cr** |

### POST /api/v1/stock-video — Stock-footage edit
Auto-edited video from a stock library, driven by a script + topic.
Optionally re-times to a supplied narration audio. **Async** — returns a
`job_id`; poll `GET /api/v1/stock-video?job_id=…` for the final
`video_url`. Credits are refunded automatically on failure.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | yes | Script / narration text (10 – 50,000 chars). Drives the credit cost. |
| `topic` | string | yes | Stock library search topic (e.g. `ocean waves`, `forest hike`). |
| `audio_url` | string | no | Public http(s) URL to narration audio (MP3/M4A/WAV, ≤25 MB). If set, the cut is re-timed to match the audio and the narration is muxed in. |
| `audio_data_uri` | string | no | Alternative to `audio_url`: base64 `data:audio/*;base64,…` URI (≤25 MB). |

**Cost:** 500 credits per 1,000 characters (minimum 500 cr).

```bash
curl -X POST "https://www.dubvoice.ai/api/v1/stock-video" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "The Pacific is the largest and deepest ocean on Earth...",
    "topic": "ocean waves",
    "audio_url": "https://example.com/narration.mp3"
  }'
```

Status poll response when ready:

```json
{
  "job_id": "abc123...",
  "status": "DONE",
  "video_url": "https://....supabase.co/storage/v1/object/public/tts-audio/videos/...mp4"
}
```

Status values: `QUEUED` → `PROCESSING` → `DONE` (or `ERROR`).

---

## tab:image — Image Generation

### POST /api/image-generate
Models (credit cost in parentheses):
- **Nano Banana 2 Lite** — `nano-banana-2-lite` (500 cr flat) — fastest / cheapest tier. 1K native. Accepts up to 4 `image_input` references. No 4K upscale.
- **Nano Banana 2** — `nano-banana-2` (1,000 cr flat) — fast general-purpose image model. 1K native. Accepts up to 4 `image_input` references. No 4K upscale.
- **Meta AI** — `meta` (1,500 cr flat) — text-to-image and image-to-image. Aspects `1:1` / `9:16` / `16:9` only. Uses named components `character_image` / `scene_image` / `style_image` (any one switches it into i2i) instead of `image_input`; a single `image_input` entry is mapped onto `character_image`.
- **Nano Banana Pro** — `nano-banana-pro` (**3,500 cr flat**, reduced from 5,000) — premium quality. 1K native. Accepts up to 4 `image_input` references; supports optional 4K upscale via `resolution: "4K"`.
- **Grok Image** — `grok-image` (1,000 cr) — xAI Grok. Text-to-image and image-to-image; supply up to 4 `image_input` references and Grok auto-switches into i2i mode. Native aspects: 1:1, 9:16, 16:9, 2:3, 3:2.
- **GPT Image 2** — `gpt-image-2` (**15,000 cr flat**, replacing the old 4,500-80,000 `mode` × `resolution` matrix) — OpenAI GPT Image 2. Photorealistic with strong text rendering. Supports **all seven aspect ratios** (`1:1`, `9:16`, `16:9`, `3:4`, `4:3`, `2:3`, `3:2`). Output is capped at ~1.57 MP and follows the aspect ratio — there is **no 2K/4K tier and no upscale**, so `resolution` is ignored. Accepts up to **5** positional `image_input` references (no `@tag` binding). Model-specific controls: `quality`, `prompt_mode`, `reasoning`, `web_search` (see the table below) — these change the output, not the price.
- **Flux 2 Pro** — `flux-2-pro` / alias `flux` (5,000 cr) — optional image input, up to 5 references.

All listed prices are final — there is no surcharge or fallback markup. Failures are refunded automatically.

**Monthly plans do not apply here.** Image Max and Veo Max make covered
models free in the dashboard only. An API-key request is always billed in
credits, and never consumes a plan's daily allowance.

**Rate limit:** **10 requests / minute per user** on `POST /api/image-generate`. Exceeding it returns `429 Too Many Requests` with a `Retry-After` header and a `limit_per_minute` field.

**Concurrency limit:** at most **3 image generations in flight per user** at any moment. A 4th request while 3 are already processing returns `429 Too Many Requests` — wait for one to finish before retrying.

**Nano Banana Pro quota (per user, rolling windows — applies ONLY to `nano-banana-pro`):**
- **Daily** — 300 images.
- **Weekly** — 6,000 images.
- **Monthly** — 30,000 images.

`nano-banana-2` is unmetered — no per-window cap. Only `nano-banana-pro` consumes the budget. Hitting any window returns `429 Too Many Requests` with a `quota_window` field (`daily` / `weekly` / `monthly`) and a `resets_at` ISO timestamp. Check your current usage via `GET /api/image-generate/quota`.

### GET /api/image-generate/quota
Returns the caller's current Nano Banana Pro usage:

```json
{
  "models": ["nano-banana-pro"],
  "limits": { "daily": 300, "weekly": 6000, "monthly": 30000 },
  "usage": {
    "daily":   { "used": 12,  "limit": 300,   "remaining": 288,   "resetsAt": "..." },
    "weekly":  { "used": 87,  "limit": 6000,  "remaining": 5913,  "resetsAt": "..." },
    "monthly": { "used": 342, "limit": 30000, "remaining": 29658, "resetsAt": "..." }
  }
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `prompt` | string | yes | Image description (max 20,000 chars). |
| `model` | string | yes | One of the model IDs above. |
| `aspect_ratio` | string | no | Model-dependent. Nano Banana family: `1:1` / `9:16` / `16:9` / `3:4` / `4:3`. Grok: adds `2:3` / `3:2`, drops `3:4` / `4:3`. Meta: `1:1` / `9:16` / `16:9` only. GPT Image 2: all seven. An unsupported ratio falls back to `16:9`. |
| `resolution` | string | no | Only meaningful as **Nano Banana Pro's** 4K upscale flag — pass `"4K"`. Every other model is 1K native and ignores it. |
| `quality` | string | no | **GPT Image 2 only.** `low` / `medium` / `high` (default `high`). Affects output only — the price is flat. |
| `prompt_mode` | string | no | **GPT Image 2 only.** `auto` (default — the model refines your prompt) / `direct` (used verbatim). |
| `reasoning` | string | no | **GPT Image 2 only.** `none` (default) / `low` / `medium` / `high` / `xhigh` / `max`. Higher follows the prompt more closely but is slower. |
| `web_search` | boolean | no | **GPT Image 2 only.** `true` grounds the generation with a web search first. Default `false`. |
| `image_input` | string[] | no | Reference images (base64 data URIs or http(s) URLs). Per-model limits: Nano Banana family 4 / Grok 4 / **GPT Image 2 5** / Flux 2 Pro 5. Meta uses the named component fields instead. |
| `reference_images` | string[] | no | Alias of `image_input`. |
| `negative_prompt` | string | no | What to avoid. |

```bash
curl -X POST "https://www.dubvoice.ai/api/image-generate" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "prompt": "A futuristic city skyline at sunset", "model": "nano-banana-2" }'
```

### GET /api/image-generate/status?id=&lt;task_id&gt;
Poll long-running image tasks. Returns `{ status, image_url, image_urls, error }`.

---

## tab:music — Music Generation

### POST /api/music-generate — 5,000 cr (returns 1–2 variations)

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `customMode` | boolean | yes | `false` = simple (only `prompt`, ≤500 chars). `true` = custom (`style` + `title` required; `prompt` = lyrics if not instrumental). |
| `instrumental` | boolean | no | `true` = no vocals. |
| `prompt` | string | yes | Idea (≤500 simple) or full lyrics (≤5000 custom+vocals). |
| `style` | string | custom | Genre / mood (≤1000 chars). |
| `title` | string | custom | Track title (≤80 chars). |
| `vocalGender` | string | no | `m` / `f` — custom mode with vocals only. |

```bash
curl -X POST "https://www.dubvoice.ai/api/music-generate" \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "customMode": true, "instrumental": false,
    "title": "Childhood Dreams", "style": "Folk, Acoustic, Nostalgic",
    "prompt": "Verse 1: When I was young...",
    "vocalGender": "f"
  }'
```

→ `{ "success": true, "task_id": "5c79...be8e" }`

### GET /api/music-generate?id=&lt;task_id&gt; — poll

```json
{
  "status": "succeeded",
  "audio_url": "https://your-storage.supabase.co/.../music_xxx.mp3",
  "audio_urls": ["...mp3", "...mp3"],
  "task_id": "5c79...be8e",
  "credits_used": 5000
}
```

---

## tab:common — Common (auth, voices, account, errors)

### Authentication
- `Authorization: Bearer sk_your_api_key` (preferred)
- `X-API-Key: sk_your_api_key` (alt header)

API keys can be created/rotated/revoked at /dashboard/api-docs. Keys are
prefixed `sk_` and shown once on creation.

### GET /api/v1/voices
List available voices with filtering.

| Query param | Description |
|-------------|-------------|
| `provider` | `elevenlabs` (default, **15,190** voices) / `minimax` (**441**) / `vbee` (**1,649**) / `fishaudio` (**275**) / `all` |
| `search` | Substring match on name |
| `language` | Exact, case-insensitive match on the voice's `language` field. ElevenLabs / Fish Audio use ISO 639-1 (`en`, `tr`); Vbee uses full names (`Vietnamese`, `Arabic`). |
| `gender` | `male` / `female` |
| `page`, `page_size` | Pagination (default 1, 30) |

`vbee` (1,649 voices, 52 languages, 1,212 of them Vietnamese) and `fishaudio` (275 voices, 15 languages) ship as bundled catalogues, so listing them needs no upstream call. Their voice ids come back already prefixed (`vbee_...`, `fishaudio_...`) and can be passed straight to `POST /api/v1/tts` — see the *Vbee & Fish Audio* tab for the full flow.

```bash
curl "https://www.dubvoice.ai/api/v1/voices?search=rachel&language=en&gender=female" \
  -H "Authorization: Bearer sk_..."
```

### GET /api/v1/me
Returns the authenticated user's profile + credits balance.

```json
{ "id": "...", "email": "...", "credits": 12345, "plan": "pro" }
```

### Rate limits
- TTS submit: 20 req/min per key
- Translate: 60 req/min per key
- Voices list: 60 req/min per key
- Image / video / music: 10 req/min per key (long-running)

429 responses include `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers.

### Error codes

| HTTP | Code | Meaning |
|------|------|---------|
| 400 | `INVALID_REQUEST` | Bad body / missing field |
| 400 | `MINIMAX_NOT_SUPPORTED_HERE` | Used a Minimax voice on `/api/v1/tts` |
| 401 | `INVALID_API_KEY` | Missing / expired / revoked key |
| 402 | — | Insufficient credits (`required` + `available` in body) |
| 404 | — | Task / voice not found |
| 429 | — | Rate limit hit (`retryIn` in body) |
| 503 | `SERVICE_MAINTENANCE` | Whole platform under maintenance |
| 503 | `FEATURE_MAINTENANCE` | Specific feature (e.g. ElevenLabs) under maintenance |
| 500 | — | Internal error (please retry) |

### Status values (TTS / video / music / image jobs)

`pending` → `processing` → `completed` (or `succeeded` for music) | `failed`.

### Webhooks
Pass `webhook_url` (HTTPS) on submit and we POST the final result there:

```json
{
  "task_id": "...",
  "status": "completed",
  "result": "https://...mp3",
  "characters": 30,
  "completed_at": "2026-05-05T12:34:56Z"
}
```

We retry up to 3× with exponential backoff if your endpoint returns non-2xx.

### Code examples

**Node (fetch)**

```js
const res = await fetch("https://www.dubvoice.ai/api/v1/tts", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.DUBVOICE_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    text: "Hello world",
    voice_id: "21m00Tcm4TlvDq8ikWAM",
    language: "auto",
  }),
});
const { task_id } = await res.json();
```

**Python (requests)**

```python
import os, requests
r = requests.post(
  "https://www.dubvoice.ai/api/v1/tts",
  headers={"Authorization": f"Bearer {os.environ['DUBVOICE_API_KEY']}"},
  json={"text": "Hello world", "voice_id": "21m00Tcm4TlvDq8ikWAM", "language": "auto"},
)
task_id = r.json()["task_id"]
```

**Polling helper**

```python
import time
while True:
    j = requests.get(
      f"https://www.dubvoice.ai/api/v1/tts?task_id={task_id}",
      headers={"Authorization": f"Bearer {api_key}"},
    ).json()
    if j["status"] in ("completed", "failed"): break
    time.sleep(3)
print(j["result"])
```
