# Text-to-Speech

Generate TTS audio and receive a permanent hosted audio URL.

Generate high-quality TTS audio programmatically. Submit a script, choose a voice, and receive a permanent hosted audio URL. Uses character-based credits (see [Credits & Billing](/docs/credits#credits)).

### `GET /api/voices`

Browse and search available ElevenLabs voices. Returns voice IDs you can use with `provider=elevenlabs`. For Stealth voices, use [/api/voices/stealth](/docs/api/voice-management#list-stealth-voices) instead.

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| Authorization | string | Required | Bearer token: `Bearer YOUR_API_KEY` |

**Query Parameters**

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| search | string | Optional | — | Search by voice name or labels |
| gender | string | Optional | — | Filter by gender: `male`, `female`, or `neutral` |
| age | string | Optional | — | Filter by age: `young`, `middle_aged`, or `old` |
| language | string | Optional | — | Language code (e.g. `en`, `es`, `fr`) |
| accent | string | Optional | — | Filter by accent (e.g. `american`, `british`) |
| sort | string | Optional | trending | Sort by: `trending`, `created_date`, `usage_character_count_1y` |
| page_size | integer | Optional | 30 | Results per page (max 100) |
| page | integer | Optional | 0 | Page number (0-indexed) |

**Example Request**

```
# Browse trending voices
curl "https://api.algrow.online/api/voices?sort=trending&page_size=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Search for female English voices
curl "https://api.algrow.online/api/voices?search=narrator&gender=female&language=en" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response 200 200**

```
{
  "success": true,
  "voices": [
    {
      "voice_id": "EkK5I93UQWFDigLMpZcX",
      "name": "James - Husky, Engaging and Bold",
      "gender": "male",
      "age": "middle_aged",
      "accent": "american",
      "language": "en",
      "description": "A slightly husky and bassy voice...",
      "preview_url": "https://...",
      "category": "high_quality",
      "use_case": "narrative_story"
    }
  ],
  "has_more": true
}
```

### `POST /api/generate-simple`

Create a text-to-speech generation job. Returns a `job_id` immediately. The audio is generated asynchronously — poll `/api/job-status/:job_id` to check progress and retrieve the audio URL when complete.

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| Authorization | string | Required | Bearer token: `Bearer YOUR_API_KEY` |
| Content-Type | string | Auto | Set automatically by `curl -F`. If manual: `multipart/form-data` |

**Request Parameters (form-data)**

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| script | string | Required | — | Text to convert to speech. Limit depends on plan & provider (see table below). |
| voice_id | string | Required | — | Voice ID. For ElevenLabs: e.g. `21m00Tcm4TlvDq8ikWAM`. For Stealth: use the `voice_id` from [/api/voices/stealth](/docs/api/voice-management#list-stealth-voices). For MiniMax: use the `voice_id` from [/api/voices/minimax](/docs/api/voice-management#list-minimax-voices) (clone first). |
| provider | string | Optional | elevenlabs | TTS engine. Values: `elevenlabs`, `stealth`, `minimax` |
| model_id | string | Optional | eleven_multilingual_v2 | Model to use. Also available: `eleven_v3`, `eleven_v4` (newest, most expressive; bills **3×** characters), `eleven_turbo_v2_5`, `eleven_flash_v2_5`, `eleven_turbo_v2`, `eleven_flash_v2` |
| stability | float | Optional | 0.5 | Voice consistency. Higher = more stable, lower = more expressive. Range: 0.0 – 1.0 |
| similarity_boost | float | Optional | 0.5 | How closely to match the original voice. Range: 0.0 – 1.0 |
| style | float | Optional | 0.0 | Speaking style exaggeration. Higher values amplify the voice's style. Range: 0.0 – 1.0 |
| speed | float | Optional | 1.0 | Playback speed. Range: 0.7 – 1.2 (ElevenLabs) or 0.5 – 2.0 (MiniMax) |
| pitch | int | Optional | 0 | Pitch shift in semitones. Range: -12 – +12. (MiniMax only) |
| volume | float | Optional | 1.0 | Output volume multiplier. Range: 0.0 – 10.0. (MiniMax only) |
| voice_name | string | Optional | voice_id | Human-readable label for this voice (for your reference only) |
| custom_title | string | Optional | — | Custom filename for the output MP3 (without extension) |
| generate_srt | string | Optional | false | ElevenLabs only: set to `true` to generate an SRT subtitle file. Bills **1.2×** characters. Ignored for Stealth, which always returns a free word-level SRT. |
| temperature | float | Optional | 1.1 | Voice expressiveness (Stealth only). Higher = more expressive. |
| speaking_rate | float | Optional | 1.0 | Speaking speed multiplier (Stealth only). |
| stealth_model | string | Optional | 1.5 | Stealth model tier (Stealth only). `1.5` = standard model (1× characters, default); `2.0` = Stealth 2.0, our newest, most capable model (2× characters). |

**Provider-specific parameters:** When `provider=stealth`: only `temperature`, `speaking_rate` and `stealth_model` are used. Parameters `stability`, `similarity_boost`, `style`, `speed`, and `model_id` are ignored. When `provider=elevenlabs` (default): only `stability`, `similarity_boost`, `style`, `speed`, and `model_id` are used. Parameters `temperature` and `speaking_rate` are ignored. When `provider=minimax`: only `speed`, `pitch`, and `volume` are used. The voice must be cloned via [/api/voices/minimax/clone](/docs/api/voice-management#clone-minimax-voice) first. Minimum 200 characters.

**Stealth models:** Two tiers, selected with `stealth_model`. `1.5` (default) is the standard model and bills **1×** characters. `2.0` is Stealth 2.0 — our newest, most capable model with richer expression and stronger multilingual quality — and bills **2×** characters against your Stealth balance. Both auto-chunk at ~1,900 char boundaries. Output: MP3, uploaded to CDN.

**SRT subtitles:** Setting `generate_srt=true` (ElevenLabs only) runs an extra forced-alignment pass to produce a word-timed subtitle file alongside the audio, and bills **1.2×** the character count of your script. Without it, generation bills **1×**. `model_id=eleven_v4` bills **3×** characters, and the two stack (v4 with SRT bills 3.6×). Stealth jobs always include a word-level SRT as `transcript_url` at no extra cost, so `generate_srt` is ignored there and billing stays at 1× the script characters (2× on Stealth 2.0).

Per-generation character limits:

| Provider | Professional | Ultimate |
| --- | --- | --- |
| ElevenLabs | 100,000 | 200,000 |
| Stealth | 45,000 | 100,000 |
| MiniMax | 100,000 | 200,000 |

**Example Request (ElevenLabs)**

```
curl -X POST "https://api.algrow.online/api/generate-simple" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "script=Hello, welcome to our channel." \
-F "voice_id=21m00Tcm4TlvDq8ikWAM" \
-F "provider=elevenlabs" \
-F "stability=0.7" \
-F "similarity_boost=0.8"
```

**Example Request (Stealth)**

```
curl -X POST "https://api.algrow.online/api/generate-simple" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "script=Hello, welcome to our channel." \
-F "voice_id=Evan" \
-F "provider=stealth" \
-F "temperature=1.1" \
-F "speaking_rate=1.0"
```

**Example Request (Stealth 2.0 — bills 2x)**

```
curl -X POST "https://api.algrow.online/api/generate-simple" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "script=Hello, welcome to our channel." \
-F "voice_id=Evan" \
-F "provider=stealth" \
-F "stealth_model=2.0" \
-F "temperature=1.1" \
-F "speaking_rate=1.0"
```

**Response 200 200**

```
{
  "success": true,
  "job_id": "d477b67a-bb9d-403e-a6cf-bc8a82c93a61",
  "status": "pending",
  "status_detail": "pending",
  "status_detail_message": "Processing",
  "message": "Generation queued. Worker will process it.",
  "payload": {
    "text": "Hello, welcome to our channel.",
    "voice_id": "21m00Tcm4TlvDq8ikWAM",
    "voice_name": "21m00Tcm4TlvDq8ikWAM",
    "settings": { ... }
  }
}
```

**Response Fields**

| Field | Type | Description |
| --- | --- | --- |
| success | boolean | Whether the request was accepted |
| job_id | string | Unique job identifier. Use this to poll for status. |
| status | string | Current job status: `pending` |
| status_detail_message | string | Human-readable status message |
| message | string | Informational message |
| payload | object | Echo of the submitted parameters (text, voice_id, settings, etc.) |

200 Queued 400 Validation error 401 Auth failed 402 No credits 403 Plan required 429 Too busy 500 Server error

### `GET /api/job-status/:job_id`

Retrieve the current status and result of a generation job. Poll this endpoint every 2–3 seconds until `status` is `completed` or `failed`. Typical generation time is 3–15 seconds depending on script length.

**Path Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| job_id | string | Required | The `job_id` returned from `POST /api/generate-simple` |

**Example Request**

```
curl "https://api.algrow.online/api/job-status/300040" \
-H "Authorization: Bearer YOUR_API_KEY"
```

**Response — In Progress**

```
{
  "success": true,
  "job_id": "d477b67a-bb9d-403e-a6cf-bc8a82c93a61",
  "status": "processing",
  "status_detail": "processing",
  "status_detail_message": "Processing",
  "created_at": 1772482202.525
}
```

**Response — Completed**

```
{
  "success": true,
  "job_id": "d477b67a-bb9d-403e-a6cf-bc8a82c93a61",
  "status": "completed",
  "status_detail": "completed",
  "status_detail_message": "Completed",
  "created_at": 1772482202.525,
  "completed_at": 1772482210.831,
  "audio_url": "https://audio.algrow.online/elevenlabs/tts/user123/300040.mp3",
  "transcript_url": "https://audio.algrow.online/elevenlabs/tts/user123/transcript_300040.srt"
}
```

**Response — Failed**

```
{
  "success": true,
  "job_id": "d477b67a-bb9d-403e-a6cf-bc8a82c93a61",
  "status": "failed",
  "status_detail": "failed",
  "status_detail_message": "Failed",
  "created_at": 1772482202.525,
  "completed_at": 1772482215.100,
  "error": "Async job failed: [TERMS_OF_SERVICE_VIOLATION] The text you are trying to use may violate our Terms of Service and has been blocked.",
  "error_code": "TERMS_OF_SERVICE_VIOLATION",
  "error_message": "The text you are trying to use may violate our Terms of Service and has been blocked."
}
```

**Response Fields**

| Field | Type | Description |
| --- | --- | --- |
| success | boolean | Always `true` if the job was found |
| job_id | string | The job identifier |
| status | string | One of: `pending`, `processing`, `completed`, `failed` |
| status_detail_message | string | Human-readable status: "Processing", "Completed", or "Failed" |
| created_at | float | Unix timestamp when the job was created |
| completed_at | float | Unix timestamp when the job finished (only present when done) |
| audio_url | string | Permanent URL to the MP3 file. After generation, audio is uploaded to Cloudflare R2 and served via our CDN at `audio.algrow.online` — this URL will not expire. (Only when `status=completed`) |
| transcript_url | string | Permanent URL to the SRT subtitle file, built from word-level timestamps. Always present for completed Stealth jobs (free). For ElevenLabs, only present when `generate_srt=true` was passed. |
| error | string | Error description (only when `status=failed`) |
| error_code | string | Machine-readable error code, e.g. `TERMS_OF_SERVICE_VIOLATION` (only when `status=failed`, if available) |
| error_message | string | Human-readable error message from the provider (only when `status=failed`, if available) |

**Status lifecycle:** `pending` → `processing` → `completed` or `failed`. Typical completion time is 3–15 seconds.

200 Success 401 Auth failed 404 Job not found

### `GET /api/jobs`

List your generation jobs, sorted by creation time (newest first). Returns only jobs belonging to the authenticated user. Useful for debugging and monitoring your recent generations.

**Example Request**

```
curl "https://api.algrow.online/api/jobs" \
-H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```
{
  "success": true,
  "jobs": [
    {
      "id": "300040",
      "provider": "elevenlabs",
      "status": "completed",
      "script_length": 340,
      "created_at": 1772482202.525,
      "completed_at": 1772482210.831
    }
  ]
}
```

### `GET /api/health`

Check API health and view your current job counts. No authentication required. Use this to verify the API is online and check how many concurrent slots you have available.

**Example Request**

```
curl "https://api.algrow.online/api/health" \
-H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```
{
  "status": "healthy",
  "active_jobs": 2,
  "total_jobs": 15
}
```

### `POST /api/reports`

Publish a self-contained HTML report to Algrow's public report host and get back a stable URL on `audio.algrow.online` — no storage credentials needed. Built for agent skills that render analysis reports (channel decodes, audits, idea backlogs) and want to hand the user a hosted link. Reports are namespaced per user: re-posting the same slug overwrites your own report (re-render = same URL); you can never touch another user's. Pages are served with `noindex` so they stay out of search engines.

**Request Body (JSON)**

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| slug | string | Required | — | URL slug for the report: 3–80 chars of `a-z 0-9 -`, starting alphanumeric. Same slug → overwrite your own previous version. |
| html | string | Required | — | The complete, self-contained HTML document (must start with `<!DOCTYPE html>` or `<html>`). Max 2 MB — inline your CSS; link external images/fonts by URL. |

**Limits.** 50 uploads per key per 24h, on top of the standard per-key rate limit. A `<meta name="robots" content="noindex">` tag is injected automatically if missing.

200 Report published 400 Bad slug / not a complete HTML document 401 Auth failed 413 HTML too large (2 MB cap) 429 Daily upload cap reached

### `GET /api/credits`

Check what your key has left to spend. Returns your studio credit balance (what image, video, thumbnail and caption-remover calls draw from) plus both voice character pools. ElevenLabs and MiniMax share one pool (`tts_characters`); Stealth has its own (`stealth_characters`). Studio credits and plan characters reset every month on `period_end`; purchased characters roll over and are only spent once the plan allowance is gone.

**Example Request**

```
curl "https://api.algrow.online/api/credits" \
-H "Authorization: Bearer YOUR_API_KEY"
```

**Response**

```
{
  "success": true,
  "plan": "professional",
  "credits": {
    "remaining": 257.5,
    "limit": 300,
    "used": 42.5,
    "period_start": "2026-08-01T09:14:22+00:00",
    "period_end": "2026-08-31T09:14:22+00:00"
  },
  "tts_characters": {
    "remaining": 120000,
    "plan_remaining": 100000,
    "plan_limit": 100000,
    "purchased_remaining": 20000
  },
  "stealth_characters": {
    "remaining": 1850000,
    "plan_remaining": 1850000,
    "plan_limit": 2000000,
    "purchased_remaining": 0
  }
}
```

**Response Fields**

| Field | Type | Description |
| --- | --- | --- |
| plan | string | Your current plan tier (`starter`, `professional` or `ultimate`). |
| credits.remaining | number | Studio credits left this period. Spent by `/api/generate-image`, `/api/generate-video`, the `/api/thumbnails` endpoints and `/api/caption-remover`. |
| credits.limit | number | Studio credits included this period, purchased credits included. |
| credits.period_end | string | ISO timestamp of the next monthly reset. |
| tts_characters.remaining | number | Characters available to `/api/generate-simple` with `provider=elevenlabs` or `provider=minimax` (plan allowance plus purchased). |
| tts_characters.purchased_remaining | number | Characters bought as credit packs. These roll over between periods. |
| stealth_characters.remaining | number | Characters available to `/api/generate-simple` with `provider=stealth`. Separate pool from `tts_characters`; a Stealth generation on the 2.0 model bills double the script length. |
| stealth_characters.purchased_remaining | number | Stealth characters bought as top-ups. These roll over between periods. |

200 Success 401 Auth failed
