# API Keys
Source: https://docs.audiopod.ai/account/api-keys
Create and manage your AudioPod AI API keys
## Create Your First Key
Already have an API key? Create more via cURL:
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "production-key", "scopes": ["*"]}'
```
**First time?** Follow the [Quick Start](/quickstart) to register and get your first key.
1. Go to [API Keys](https://www.audiopod.ai/dashboard/account/api-keys)
2. Click **Create New API Key**
3. Copy and save your key immediately
Keys are only shown once. Save it securely!
***
## Using Your API Key
### Environment Variable (Recommended)
```bash theme={null}
export AUDIOPOD_API_KEY="ap_your_key_here"
```
### SDK Usage
```python theme={null}
from audiopod import AudioPod
# Auto-reads from AUDIOPOD_API_KEY env var
client = AudioPod()
# Or pass explicitly
client = AudioPod(api_key="ap_your_key_here")
```
```typescript theme={null}
import AudioPod from 'audiopod';
// Auto-reads from AUDIOPOD_API_KEY env var
const client = new AudioPod();
// Or pass explicitly
const client = new AudioPod({ apiKey: 'ap_your_key_here' });
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=VIDEO_ID" \
-F "mode=six"
```
***
## Manage Keys via API
```bash theme={null}
# List all keys
curl -s "https://api.audiopod.ai/api/v1/auth/api-keys" \
-H "X-API-Key: $AUDIOPOD_API_KEY" | jq .
# Create new key
curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "staging-key",
"scopes": ["*"],
"expires_in_days": 90
}'
# Revoke a key
curl -X DELETE "https://api.audiopod.ai/api/v1/auth/api-keys/KEY_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
***
## Best Practices
```bash theme={null}
# Development
AUDIOPOD_API_KEY_DEV=ap_dev_...
# Production
AUDIOPOD_API_KEY_PROD=ap_prod_...
```
API keys should only be used server-side. Create a backend proxy if you need client-side access.
Create a new key → Update your apps → Revoke the old key.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-d '{"name": "temp-key", "expires_in_days": 7}'
```
***
## Troubleshooting
| Error | Cause | Fix |
| - | - | - |
| `401 Unauthorized` | Invalid or missing key | Check `X-API-Key` header |
| `403 Forbidden` | Key revoked or expired | Create a new key |
Test your key:
```bash theme={null}
curl -s "https://api.audiopod.ai/api/v1/api-wallet/balance" \
-H "X-API-Key: $AUDIOPOD_API_KEY" | jq .balance_usd
```
***
## Next Steps
Top up your wallet to start making API calls
Make your first API call
# API Wallet & Pricing
Source: https://docs.audiopod.ai/account/api-wallet
Pay-as-you-go pricing. Add funds and pay only for what you use.
## Pricing
**The developer API wallet is funded in USD and is separate from Studio subscription or PAYG credits.** Studio's \$1 = 7,500-credit conversion does not apply to the API wallet. See the live [API pricing page](https://audiopod.ai/developers/pricing) for current rates.
Text to Speech, Voice Cloning and Audiobook Narration are priced per 1,000
characters of input text. Every other API price below is per minute of audio
processed (input duration unless noted).
| Service | Rate | \$10 Gets You |
| - | - | - |
| **Stem Separation — core outputs** | \$0.10/min | 100 min |
| **Transcription** | \$0.01/min | 1000 min |
| **Text to Speech** | \$0.05 per 1,000 characters | \~200,000 characters |
| **Voice Cloning** | \$0.05 per 1,000 characters | \~200,000 characters |
| **Voice Conversion** | \$0.13/min | 77 min |
| **Speech Translation (Dubbing)** | \$0.40/min | 25 min |
| **Speaker Separation** | \$0.20/min | 50 min |
| **Music Generation** | \$0.04/min | 250 min |
| **Audiobook Narration** | \$0.09 per 1,000 characters | \~111,000 characters |
| **Media Conversion** | \$0.01/min | 1000 min |
### How characters are counted
Character-priced requests are charged from your prepaid wallet based on the text
as written, with a minimum charge of **\$0.01 per text-to-speech request**.
* Directing/steering tags and pause tags are **not** counted.
* Non-verbal tags such as `[laugh]` **are** counted.
* Everything else — letters, spaces, punctuation — counts as written.
For example, \$5 covers about 100,000 characters of text to speech or about
55,500 characters of audiobook narration.
Minimum top-up: **$1.00**. Maximum: **$10,000**.
**Premium variants and modes.** Some services charge a multiplier on the base
rate:
* **Music Generation — AudioMusic Premium** (`quality="premium"`): **2×** the
base rate (\$0.08/min).
* **Stem Separation — output groups**: lead + backing vocals cost **$0.15/min together**; male + female vocals cost **$0.15/min together**. Both pairs total **$0.30/min**. Other specialist instrument groups cost **$0.20/min each**; lead + rhythm guitar share one group. Add **\$0.10/min** if you also select core stems. Selecting more outputs within one group does not add another charge. [Group pricing details](/api-reference/stem-splitter#prices-by-output-group).
Every standard mode — `single`, `two`, `four`, `six`, `producer`, `studio`,
`mastering` — bills at the base \$0.10/min.
**TTS billing model.** API wallet bills TTS by the **duration of the generated
audio**, not by character count. The per-character rate shown elsewhere in the
docs applies to account-credit billing (subscription plans), not to API-wallet
billing.
**Noise Reduction** API-key calls bill the API price of the chosen
`quality_mode` (`balanced`, `studio` or `ultra`). Call
`POST /api/v1/denoiser/quote` for the exact cost before you start a job. See
[Noise Reduction](/api-reference/noise-reduction#pricing).
***
## Check Balance
```bash theme={null}
curl -s "https://api.audiopod.ai/api/v1/api-wallet/balance" \
-H "X-API-Key: $AUDIOPOD_API_KEY" | jq .
```
```python theme={null}
from audiopod import AudioPod
client = AudioPod()
balance = client.wallet.get_balance()
print(f"Balance: {balance['balance_usd']}")
print(f"Spent: {balance['total_spent_usd']}")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod();
const balance = await client.wallet.getBalance();
console.log(`Balance: ${balance.balance_usd}`);
console.log(`Spent: ${balance.total_spent_usd}`);
```
**Response:**
```json theme={null}
{
"balance_cents": 2500,
"balance_usd": "$25.00",
"total_spent_cents": 1500,
"total_spent_usd": "$15.00",
"low_balance_warning": false
}
```
***
## Add Funds
```bash theme={null}
# Get Stripe payment link ($25)
curl -X POST "https://api.audiopod.ai/api/v1/api-wallet/topup/checkout" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount_cents": 2500}'
```
Open the returned `url` in your browser to complete payment.
1. Go to [API Keys](https://www.audiopod.ai/dashboard/account/api-keys)
2. Click **Add Funds**
3. Choose amount and complete Stripe checkout
**Response:**
```json theme={null}
{
"url": "https://checkout.stripe.com/c/pay/...",
"amount_usd": "$25.00"
}
```
***
## Automatic Top-Up
Automatic top-up keeps long-running and unattended integrations from stalling
on an empty wallet. Once it is on, we watch your balance as calls come in — the
moment it would drop below your **threshold**, we charge a saved card for your
chosen **amount** and credit the wallet.
The wallet stays prepaid: funds are added *before* they are spent. Nothing is
ever billed after the fact, and every automatic charge sends you a receipt by
email.
### One-time browser step
A card can only be saved from a browser. Card details are entered on a secure
hosted form run by our payment provider — they never pass through the AudioPod
API, and they can never be sent with an API key. This is a card-industry
requirement (and the reason bank verification prompts can appear), not an
AudioPod restriction.
So arming automatic top-up is a two-part flow:
1. **Once, in a browser** — save a card, either from
[API Keys](https://www.audiopod.ai/dashboard/account/api-keys) →
**Automatic top-up** → **Add card**, or by calling
`POST /auto-topup/payment-method-session` from a signed-in session and
opening the returned `url`.
2. **From then on, over the API** — read the configuration, tighten the limits,
or switch it off with a plain API key.
**API keys have deliberately limited power here.** With an API key you can
read the configuration, **lower** any limit, and **disable** automatic top-up.
Turning it **on**, **raising** a limit, or saving a card requires a signed-in
dashboard session and returns `403` with
`detail.error = "dashboard_required"` otherwise. A leaked key can therefore
never arm or widen a charge against your card — only shrink or stop one.
### Read the configuration
```bash theme={null}
curl -s "https://api.audiopod.ai/api/v1/api-wallet/auto-topup" \
-H "X-API-Key: $AUDIOPOD_API_KEY" | jq .
```
**Response:**
```json theme={null}
{
"enabled": true,
"threshold_cents": 1000,
"threshold_usd": "$10.00",
"topup_amount_cents": 2500,
"topup_amount_usd": "$25.00",
"max_topups_per_day": 3,
"monthly_cap_cents": 20000,
"monthly_cap_usd": "$200.00",
"has_payment_method": true,
"card_brand": "visa",
"card_last4": "4242",
"day_charge_count": 1,
"month_charged_cents": 2500,
"last_charged_at": "2026-07-30T14:02:11Z",
"next_attempt_at": null,
"consecutive_failures": 0,
"disabled_reason": null,
"disabled_at": null,
"limits": {
"min_threshold_cents": 500,
"max_threshold_cents": 50000,
"min_amount_cents": 1000,
"max_amount_cents": 50000,
"max_per_day": 20,
"min_monthly_cap_cents": 2500,
"max_monthly_cap_cents": 500000
}
}
```
Read your bounds from `limits` rather than hard-coding them — they are the
authoritative ranges and can change without a breaking API version.
### Update the configuration
Every field is optional; send only what you want to change.
```bash theme={null}
curl -X PUT "https://api.audiopod.ai/api/v1/api-wallet/auto-topup" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"threshold_cents": 2000, "monthly_cap_cents": 10000}'
```
| Field | Type | Default | Range |
| - | - | - | - |
| `enabled` | boolean | `false` | — (turning on needs a dashboard session) |
| `threshold_cents` | integer | `1000` (\$10) | $5 – $500 |
| `topup_amount_cents` | integer | `2500` (\$25) | $10 – $500 |
| `max_topups_per_day` | integer | `3` | 1 – 20 |
| `monthly_cap_cents` | integer | `20000` (\$200) | $25 – $5,000 |
Returns the full configuration object shown above.
**The automatic minimum ($10) is higher than the manual minimum ($1)** on
purpose. Card processing has a flat component, so a \$1 automatic recharge
spends a third of itself on fees. Larger, less frequent top-ups keep more of
your money in the wallet.
### Save or replace a card
Call this from a signed-in dashboard session and send the browser to the
returned `url`. The link is single-use and short-lived; `expires_at` is a Unix
timestamp.
```bash theme={null}
curl -X POST \
"https://api.audiopod.ai/api/v1/api-wallet/auto-topup/payment-method-session" \
-H "Content-Type: application/json"
```
**Response:**
```json theme={null}
{
"url": "https://",
"expires_at": 1785000000
}
```
### Turn it off
`DELETE` switches automatic top-up off **and** forgets the saved card. It
returns the resulting configuration, with `enabled: false` and
`has_payment_method: false`. Your balance and manual top-up are untouched.
```bash theme={null}
curl -X DELETE "https://api.audiopod.ai/api/v1/api-wallet/auto-topup" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
To pause automatic top-up but keep the card, send
`PUT {"enabled": false}` instead.
### Guardrails
Two independent ceilings bound what a runaway loop can spend. Both are enforced
server-side, and a charge that would cross either is simply not placed.
| Guardrail | What it caps | Resets |
| - | - | - |
| `max_topups_per_day` | Number of automatic charges | Daily |
| `monthly_cap_cents` | Total dollars charged automatically | On the 1st of each calendar month |
`day_charge_count` and `month_charged_cents` in the response tell you how much
headroom is left against each. When a ceiling is reached, automatic top-up stops
for the period but stays armed — nothing is disabled, and manual top-up still
works.
### When a charge fails
A declined charge is retried on a widening schedule, then automatic top-up
switches itself off rather than hammering your card:
1. **First failure** — retried about an hour later.
2. **Second failure** — retried about six hours after that.
3. **Third failure** — automatic top-up is switched off and we email you.
While a retry is pending, `next_attempt_at` holds the earliest time we will try
again and `consecutive_failures` counts the run. A successful charge resets both.
Some failures skip the retries and disable immediately, because a retry cannot
possibly clear them — most commonly when your bank requires you to confirm the
card in person, which is impossible for a charge placed while you are away.
When automatic top-up disables itself, `disabled_reason` and `disabled_at`
explain what happened. Save the card again (or replace it) and turn automatic
top-up back on.
**Cards issued in India are not supported for automatic top-up yet.** Indian
regulations require a separately registered mandate with its own notification
and approval steps, which we have not built. Saving an Indian card for
automatic top-up is refused at the point of saving. **Manual top-up is
unaffected** — Add Funds and `POST /api-wallet/topup/checkout` work normally
with an Indian card.
***
## Estimate Cost
Calculate cost before processing (no auth required):
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/api-wallet/estimate" \
-H "Content-Type: application/json" \
-d '{"service_type": "stem_extraction", "duration_seconds": 300}'
```
**Response:**
```json theme={null}
{
"service_type": "stem_extraction",
"duration_minutes": 5.0,
"rate_per_minute": "$0.10/min",
"cost_usd": "$0.50"
}
```
***
## Usage History
```bash theme={null}
curl -s "https://api.audiopod.ai/api/v1/api-wallet/usage?limit=10" \
-H "X-API-Key: $AUDIOPOD_API_KEY" | jq .
```
**Response:**
```json theme={null}
{
"logs": [
{
"service_type": "stem_extraction",
"duration_minutes": 5.47,
"amount_usd": "$0.55",
"created_at": "2025-12-11T12:39:37Z"
}
]
}
```
***
## Get Pricing (No Auth)
```bash theme={null}
curl -s "https://api.audiopod.ai/api/v1/api-wallet/pricing" | jq .services
```
***
## Error Handling
| Code | Error | Meaning |
| - | - | - |
| `402` | `insufficient_api_balance` | Wallet is empty |
| `400` | `invalid_amount` | Amount outside $1-$10,000 range |
| `401` | `unauthorized` | Invalid API key |
| `403` | `dashboard_required` | Automatic top-up: this change (turning it on, raising a limit, or saving a card) needs a signed-in dashboard session |
**Example 402 response:**
```json theme={null}
{
"error": "insufficient_api_balance",
"message": "Insufficient balance. Required: $0.50, Available: $0.00",
"required_cents": 50,
"available_cents": 0
}
```
***
## Next Steps
Make your first API call
Most popular API
# Webhooks
Source: https://docs.audiopod.ai/account/webhooks
Receive a signed HTTP callback the moment an AudioPod job finishes — no polling. Register an endpoint, verify the signature, and handle retries.
## Overview
AudioPod processing is asynchronous: you create a job, it runs, and it finishes a few seconds to a few minutes later. Instead of polling the job status, register a **webhook** and AudioPod will `POST` a signed event to your server the moment the job completes or fails.
Every delivery is **HMAC-signed**, retried with exponential backoff, and carries a unique event id so you can safely deduplicate.
## Events
| Event | When it fires |
| - | - |
| `job.completed` | A job finished successfully and its output is available. |
| `job.failed` | A job failed. Credits reserved for the job are refunded automatically. |
| `job.cancelled` | A job was stopped on request (e.g. `POST .../narration/cancel`). Charges held for work that did not complete are released; output already produced is kept. |
| `narration.review_required` | An audiobook narration job finished with passages waiting for your [review](/api-reference/audiobook#narration-review). Fires once per job. |
| `webhook.test` | A synthetic event you trigger yourself to confirm your endpoint works. |
## Register an endpoint
Create an endpoint with the URL to call and the events you want. The response includes a **signing secret** — it is shown **once**, so store it now.
```bash theme={null}
curl -X POST https://api.audiopod.ai/api/v1/webhooks/endpoints \
-H "Authorization: Bearer $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/audiopod",
"events": ["job.completed", "job.failed"],
"description": "Production job notifications"
}'
```
```json theme={null}
{
"id": "a5500e11-2d26-4d87-a020-03b730dfa340",
"url": "https://your-app.com/hooks/audiopod",
"events": ["job.completed", "job.failed"],
"active": true,
"description": "Production job notifications",
"created_at": "2026-06-30T11:19:43Z",
"secret": "whsec_…"
}
```
Your endpoint **must use HTTPS** and must resolve to a public address. AudioPod refuses URLs that resolve to private, loopback, or internal addresses (SSRF protection), re-checked at delivery time.
## Event payload
The body is JSON. Every event carries `event_id` and `event_type`; job events add the job context.
```json theme={null}
{
"event_type": "job.completed",
"event_id": "f0a1…",
"job_id": 999001,
"job_type": "text_to_speech",
"status": "completed"
}
```
A `job.failed` event adds `error_message` and `failure_reason`. The `job_type` field tells you which tool produced the job. Completion webhooks now fire across the audio job types — for example `transcription`, `text_to_speech`, `voice_cloning`, `voice_conversion`, `music_generation`, `stem_extraction`, `speaker_diarization`, `denoise`, `reader`, `podcast`, `media_converter`, `audiobook`, and `export_acx`.
### narration.review\_required
Not subscribed by default — add it to `events` when you create or update an endpoint. It fires once when an audiobook narration job finishes with passages held for [narration review](/api-reference/audiobook#narration-review):
```json theme={null}
{
"event_type": "narration.review_required",
"event_id": "c7d2…",
"project_id": "3f9b…",
"job_id": 1042,
"job_type": "audiobook",
"needs_review": 2,
"paragraph_ids": ["7d0c…", "91aa…"]
}
```
Fetch the details with `GET /api/v1/audiobook/projects/{project_id}/review`, then approve or reject each passage. Export is refused until they are resolved.
## HTTP headers
| Header | Description |
| - | - |
| `Content-Type` | Always `application/json`. |
| `X-AudioPod-Signature` | `sha256=` — the HMAC signature (see below). |
| `X-AudioPod-Timestamp` | Unix seconds; part of the signed material. |
| `X-AudioPod-Event` | The event type, e.g. `job.completed`. |
| `X-AudioPod-Event-Id` | Unique id for this event — use it to deduplicate. |
## Verify the signature
The signature is `HMAC-SHA256(secret, "{timestamp}.{raw_request_body}")`, hex-encoded. Recompute it over the **raw** body bytes you received and the `X-AudioPod-Timestamp` header, then compare in constant time. Reject anything that doesn't match.
```python Python theme={null}
import hmac, hashlib
def verify(secret: str, raw_body: bytes, timestamp: str, header_sig: str) -> bool:
expected = hmac.new(
secret.encode(),
timestamp.encode() + b"." + raw_body,
hashlib.sha256,
).hexdigest()
# header_sig looks like "sha256="
received = header_sig.split("=", 1)[-1]
return hmac.compare_digest(expected, received)
# FastAPI / Flask: read the RAW body, not a re-serialized dict.
```
```javascript Node.js theme={null}
import crypto from "crypto";
function verify(secret, rawBody, timestamp, headerSig) {
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const received = headerSig.split("=")[1] || "";
return (
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
);
}
// Express: use express.raw({ type: "application/json" }) so rawBody is the
// exact bytes AudioPod signed.
```
Sign over the **raw bytes** you received, not a parsed-then-re-serialized object. Re-serializing can reorder keys or change whitespace and break the comparison.
## Respond fast, deduplicate, and retry-proof
* **Return `2xx` quickly.** Acknowledge within **10 seconds**, then do slow work asynchronously. Any non-`2xx` (or a timeout) is treated as a failed attempt.
* **Deduplicate on `X-AudioPod-Event-Id`.** Retries reuse the same event id, so store processed ids and ignore repeats.
* **Retries:** failed deliveries retry with exponential backoff — 1m, 2m, 4m, … capped at 1h — up to 5 attempts. After that the delivery is dead-lettered and can be replayed from the delivery log.
## Manage endpoints
| Action | Request |
| - | - |
| List endpoints | `GET /api/v1/webhooks/endpoints` |
| Get one | `GET /api/v1/webhooks/endpoints/{id}` |
| Update (url / events / active / description) | `PATCH /api/v1/webhooks/endpoints/{id}` |
| Delete | `DELETE /api/v1/webhooks/endpoints/{id}` |
| Send a test event | `POST /api/v1/webhooks/endpoints/{id}/test` |
| View delivery log | `GET /api/v1/webhooks/endpoints/{id}/deliveries` |
| Replay a delivery | `POST /api/v1/webhooks/deliveries/{id}/redeliver` |
### Test your integration
```bash theme={null}
curl -X POST https://api.audiopod.ai/api/v1/webhooks/endpoints/$ENDPOINT_ID/test \
-H "Authorization: Bearer $AUDIOPOD_API_KEY"
```
This delivers a `webhook.test` event so you can confirm your signature verification and `2xx` response before wiring it to real jobs. Check `GET …/deliveries` to see the recorded status and response code.
# Account API
Source: https://docs.audiopod.ai/api-reference/account
Manage your AudioPod AI account and profile
## Get Profile
```bash theme={null}
curl -X GET "https://api.audiopod.ai/api/v1/auth/me" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Response:**
```json theme={null}
{
"id": "uuid",
"email": "dev@example.com",
"full_name": "Developer Name",
"is_verified": true,
"created_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
```
***
## Update Profile
```bash theme={null}
curl -X PATCH "https://api.audiopod.ai/api/v1/auth/me" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"full_name": "New Name"
}'
```
***
## Change Password
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/change-password" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"current_password": "OldPass123",
"new_password": "NewPass456"
}'
```
**Password Requirements:**
* Minimum 6 characters
* At least one letter
* At least one number
***
## Quick Reference
| Endpoint | Method | Description |
| - | - | - |
| `/auth/me` | GET | Get profile |
| `/auth/me` | PATCH | Update profile |
| `/auth/change-password` | POST | Change password |
***
## Next Steps
Manage API keys
Check balance & pricing
# Audio to MIDI
Source: https://docs.audiopod.ai/api-reference/audio-to-midi
Convert audio — or stems you already separated — into MIDI using AudioPod's audio-to-MIDI engine.
## Overview
AudioPod's audio-to-MIDI engine transcribes bass, vocals, and piano in a
recording into note-level MIDI — bass is the strongest instrument. Guitar is
supported too but is opt-in/experimental. Use it **standalone** on a full mix
(we split it into stems first, then transcribe each requested stem) or as an
**add-on** to a stem-separation job you already ran (transcribe-only, no
re-split).
**This is a starting-point transcription, not a note-perfect one.** Bass is
the strongest instrument; expect to tidy timing and note lengths by ear in
your DAW. Dynamics (velocity) are approximate. Drums aren't transcribed
yet — a requested `drums` (or `other`) stem is accepted but comes back in
`skipped_stems`. Guitar is opt-in and experimental — it over-detects on busy
mixes, so it isn't requested by default. Detected tempo is surfaced as
`detected_tempo_bpm`.
### Key Features
* **Standalone or add-on**: convert a raw file/URL, or transcribe stems from
an existing [stem-separation](/api-reference/stem-splitter) job
* **Per-stem + multitrack output**: individual `.mid` per stem, plus a merged
multitrack `.mid` with tempo metadata
* **Tempo detection**: auto-detected via beat tracking, or pass an explicit
`tempo_bpm` override
* **Presigned downloads**: direct URLs for every output file
## Installation
```bash theme={null}
pip install audiopod
```
```bash theme={null}
npm install audiopod
```
## Authentication
Get your API key from the [API Keys Dashboard](https://www.audiopod.ai/dashboard/account/api-keys).
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Or set AUDIOPOD_API_KEY environment variable
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Or set AUDIOPOD_API_KEY environment variable
```
```bash theme={null}
export AUDIOPOD_API_KEY="ap_your_api_key"
# Use -H "X-API-Key: $AUDIOPOD_API_KEY" in requests
```
***
## Convert From a File or URL
Standalone conversion splits the mix into stems, then transcribes each
requested stem. Provide either `file` or `url` — not both. `stems` defaults
to `["bass", "vocals", "piano"]` — the strongest instruments — when omitted.
`guitar` is supported but opt-in/experimental (it over-detects on busy
mixes), so pass it explicitly if you want it. `drums` and `other` are
accepted but are never transcribed — they always come back in
`skipped_stems`.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/midi/convert" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "file=@/path/to/song.mp3"
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/midi/convert" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=VIDEO_ID" \
-F 'stems=["bass","vocals","piano","guitar"]'
```
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# From a local file (or pass url="https://.../song.mp3")
# Default stems: bass, vocals, piano
job = client.midi.convert(file="/path/to/song.mp3")
print(f"Multitrack: {job['merged_midi_url']}")
for stem, url in (job["midi_urls"] or {}).items():
print(f" {stem}: {url}")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// From a local file (or pass { url: 'https://.../song.mp3' })
// Default stems: bass, vocals, piano
const job = await client.midi.transcribe({ file: '/path/to/song.mp3' });
console.log(`Multitrack: ${job.merged_midi_url}`);
for (const [stem, url] of Object.entries(job.midi_urls ?? {})) {
console.log(` ${stem}: ${url}`);
}
```
### Options
Tunables forwarded to the audio-to-MIDI engine, passed as `options` (a JSON
object):
| Field | Type | Default | Description |
| - | - | - | - |
| `onset_threshold` | float (0.05–0.95) | `0.5` | Note-onset detection sensitivity |
| `frame_threshold` | float (0.05–0.95) | `0.3` | Frame-level pitch confidence threshold |
| `min_note_len_ms` | float (10–2000) | `128.0` | Minimum note duration in milliseconds |
| `tempo_bpm` | float (30–300) or `null` | `null` (auto-detect) | Override the detected tempo |
## Convert From an Existing Stem-Separation Job
If you already ran [stem separation](/api-reference/stem-splitter), transcribe
its output directly — no re-split, so it bills the add-on rate only. `stems`
must be a subset of the stems the source job produced, and defaults to all
of them when omitted — `drums`/`other` stems still come back in
`skipped_stems` (never transcribed), and `guitar` remains
opt-in/experimental.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/midi/convert/from-stems/5512" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"stems": ["vocals", "bass"]}'
```
```python theme={null}
stem_job = client.stems.separate(file="/path/to/song.mp3", mode="six")
job = client.midi.convert_from_stem_job(
stem_job_id=stem_job["id"],
stems=["vocals", "bass"],
)
print(job["merged_midi_url"])
```
```typescript theme={null}
const stemJob = await client.stems.separate({ file: '/path/to/song.mp3', mode: 'six' });
const job = await client.midi.convertFromStemJob(stemJob.id, {
stems: ['vocals', 'bass'],
});
console.log(job.merged_midi_url);
```
If the source stem job's output has expired under your plan's [retention
window](https://www.audiopod.ai/pricing), this returns `410 Gone` — re-run
stem separation first.
***
## Job Status & Stages
```bash theme={null}
curl -X GET "https://api.audiopod.ai/api/v1/midi/status/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
```python theme={null}
status = client.midi.get_job(job_id)
print(f"Status: {status['status']}, stage: {status['stage']}")
```
```typescript theme={null}
const status = await client.midi.status(jobId);
console.log(`Status: ${status.status}, stage: ${status.stage}`);
```
`stage` tracks pipeline progress for a `PROCESSING` job:
| Stage | Description |
| - | - |
| `split` | Separating the mix into stems (standalone mode only) |
| `transcribe` | Running the audio-to-MIDI engine on each requested stem |
| `merge` | Building the merged multitrack `.mid` |
### Other Job Endpoints
```bash theme={null}
# List jobs
curl -X GET "https://api.audiopod.ai/api/v1/midi/jobs?skip=0&limit=50" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
# Delete a job (purges its outputs)
curl -X DELETE "https://api.audiopod.ai/api/v1/midi/jobs/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
***
## Output Format
**Completed Job Response:**
```json theme={null}
{
"id": 5512,
"status": "COMPLETED",
"progress": 100,
"stage": "merge",
"mode": "standalone",
"source_job_id": null,
"stems_requested": ["bass", "vocals", "piano"],
"midi_urls": {
"bass": "https://media.audiopod.ai/...",
"vocals": "https://media.audiopod.ai/...",
"piano": "https://media.audiopod.ai/..."
},
"merged_midi_url": "https://media.audiopod.ai/...",
"stem_urls": {
"bass": "https://media.audiopod.ai/...",
"vocals": "https://media.audiopod.ai/..."
},
"detected_tempo_bpm": 118.0,
"skipped_stems": [],
"failed_stems": null,
"duration": 214,
"original_filename": "song.mp3",
"created_at": "2026-07-24T12:39:37.379848Z",
"completed_at": "2026-07-24T12:41:02.104213Z"
}
```
* **`midi_urls`** — presigned download URL per transcribed stem's `.mid` file
* **`merged_midi_url`** — a single multitrack `.mid` with all transcribed
stems as separate tracks plus tempo metadata
* **`stem_urls`** — the separated stem *audio* (standalone mode only, so you
can preview what was transcribed)
* **`skipped_stems`** — stems requested but not transcribed (always `drums`
and `other`, if requested — they're not part of the transcribable set)
* **`failed_stems`** — `{stem: error_message}` for any stem the engine
couldn't transcribe; the job still completes if at least one stem succeeded
All download URLs are presigned and expire — call the status endpoint again
to mint fresh ones.
***
## Status Codes
| Status | Description |
| - | - |
| `PENDING` | Job queued, waiting for worker |
| `PROCESSING` | Splitting and/or transcribing — see `stage` |
| `COMPLETED` | At least one stem transcribed — download URLs available |
| `FAILED` | No stem could be transcribed — check `error_message` |
***
## Pricing
See [audiopod.ai/pricing](https://www.audiopod.ai/pricing) for current rates,
or check your estimated cost before converting via the [API Wallet](/account/api-wallet).
***
## Next Steps
Separate a mix into stems before (or instead of) converting to MIDI.
Manage billing and view usage history.
# Audiobook
Source: https://docs.audiopod.ai/api-reference/audiobook
Turn a manuscript into a finished, ACX-compliant audiobook through a fully automated API: create a project, upload a PDF or EPUB, narrate it, and download a retail-ready package — with completion webhooks for hands-off, backlog-scale production.
## Overview
The Audiobook API exposes the **complete production pipeline** end to end, so you can drop AI audiobook generation directly into your own platform or control panel: a file goes in, a finished, ACX-compliant package comes back, with no human in the loop. It is the same API surface our own Audiobook Studio runs on — nothing here is a private endpoint.
```
create project → add manuscript → parse into chapters → narrate → export ACX package → download
└──── completion webhook ────┘
```
The API-key workflow supports asynchronous production: submit a job, get an ID back immediately, and receive a signed webhook when it finishes — so your control panel never blocks on a long render. The guided narrator-sample endpoint described below is Studio-only.
### Key features
* **Inputs**: upload PDF, EPUB, DOCX, or TXT; paste raw text; or generate a book from a prompt. Chapters are detected automatically.
* **Voices**: 100+ languages with standard narration voices; custom voice cloning across 20+ languages.
* **Auto Direction**: a per-paragraph performance note and emotion written across the whole book, anchored to a narration brief so the tone holds from first chapter to last — dialogue directed as dialogue, instead of a flat read. Free on every plan.
* **ACX-compliant export**: per-chapter MP3 at **192 kbps CBR / 44.1 kHz**, a retail sample, optional narrated opening/closing credits, a cover slot, and a `metadata.json` manifest — packaged as a single ZIP via a presigned URL.
* **Automatic compliance check**: file format, audio levels, silence, and per-chapter length are validated on export; a non-compliant book is rejected with the exact reason rather than shipped silently.
* **Asynchronous + webhooks**: `job.completed` / `job.failed` / `job.cancelled` callbacks for backlog-scale automation.
* **White-label output**: delivered files carry the publisher's own title/author/narrator metadata — nothing points back at AudioPod.
* **Prepaid usage pricing**: $0.09 per 1,000 characters of manuscript text (~$50 for a 10-hour book, at about 56,000 characters per finished hour). See [API Wallet](/account/api-wallet).
## Authentication
All endpoints require authentication. Use an API key (recommended for server-to-server integrations):
* **API Key (Recommended)**: `X-API-Key: your_api_key` header — [create one in the dashboard](https://www.audiopod.ai/dashboard/account/api-keys).
* **JWT Token**: `Authorization: Bearer your_jwt_token` (session-based auth).
## Account audiobook settings
`GET /api/v1/audiobook/settings` reads the authenticated account's preference for automatic reading direction in Audiobook Studio. `PATCH /api/v1/audiobook/settings` saves it and returns the same response shape.
```bash theme={null}
# Read the account preference using session authentication
curl "https://api.audiopod.ai/api/v1/audiobook/settings" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Turn automatic direction off for future Studio parses
curl -X PATCH "https://api.audiopod.ai/api/v1/audiobook/settings" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"auto_direction_default": false}'
```
The PATCH body requires `auto_direction_default`, a boolean. Both endpoints return:
```json theme={null}
{
"auto_direction_default": false,
"source": "account",
"active": true,
"applies_to_api_key_parses": false
}
```
| Field | Meaning |
| - | - |
| `auto_direction_default` | The author's saved preference. It is `true` if the account has never chosen. |
| `source` | `account` after an explicit choice; `default` if the account has never chosen. |
| `active` | Whether AudioPod currently applies account preferences to Studio parses. This is separate from the author's choice. |
| `applies_to_api_key_parses` | Always `false`. API-key parses remain opt-in, regardless of this preference. |
When `active` is `false`, PATCH still saves the preference and returns the saved value. Studio parses with no explicit `auto_direction` then default to `false`; the saved choice takes effect when `active` becomes `true`. Changing this preference does not direct or re-render existing books.
For a single manuscript import, pass `auto_direction=true` or `auto_direction=false` explicitly to override the default without changing the saved preference. This applies to uploaded-file parsing, pasted text, and generated manuscripts. Omit the field or pass JSON `null` to use the default.
The uploaded-file parse endpoint accepts this in JSON, form fields, or the query string; an explicit body value takes precedence over the query value. The paste and generate endpoints accept it in their JSON bodies.
### Guided narrator samples in Studio
The Studio-only `POST /api/v1/audiobook/projects/{project_id}/production/quote` request also accepts an optional `auto_direction` boolean. Omitted or `null` follows the author's account preference; an account with no saved preference keeps the guided workflow's default of `true`. An explicit `true` or `false` overrides that choice for this production.
This guided sample default applies independently of the `active` field for automatic direction during manuscript intake. A saved `false` is respected in both workflows. API-key requests to the guided quote endpoint return `400`; use the API narration workflow documented below.
Preparing a guided sample takes over any outstanding automatic direction started by manuscript intake, so the sample does not wait for a whole-book direction pass. A direction run started explicitly is preserved: finish or stop it before preparing the sample, otherwise the quote request returns `409`.
## Getting the IDs
Almost every call below is addressed by an ID, and how you obtain them is the first thing to get right.
**Every audiobook identifier is a UUID string**, and in every response body the field is called **`id`** — not `project_id` or `chapter_id`. Those names appear only as *path parameters* (`/projects/{project_id}/chapters/{chapter_id}`) and as *back-references* on child objects (a chapter carries `project_id`; a paragraph carries both `chapter_id` and `project_id`). So you read `id` from the object you just created, and you *write* it into the next request's path.
`GET /audiobook/projects/{project_id}` does **not** return chapters by default — the project body carries `total_chapters` (a count) and `chapters: null`. To get the chapter objects (and therefore their `id`s) you must either pass **`?include_chapters=true`** on the project read, or call `GET /audiobook/projects/{project_id}/chapters` separately.
The full call chain, with the ID each step produces:
| # | Call | Gives you |
| - | - | - |
| 1 | `POST /audiobook/projects` | `id` → your **project\_id** |
| 2 | `POST /audiobook/projects/{project_id}/manuscript/upload` | `file_key` |
| 3 | `POST /audiobook/projects/{project_id}/manuscript/parse` | `job_id` (parsing runs in the background) |
| 4 | `GET /audiobook/projects/{project_id}?include_chapters=true` | poll until parsing finishes — see [Poll for parse completion](#poll-for-parse-completion) |
| 5 | `GET /audiobook/projects/{project_id}/chapters` (or the `chapters` array from step 4) | each chapter's `id` → **chapter\_id** |
| 6 | `GET /audiobook/projects/{project_id}/chapters/{chapter_id}/paragraphs` | each paragraph's `id` → **paragraph\_id** |
| 7 | `POST /audiobook/projects/{project_id}/narration/batch` (whole book / chapters) or `POST /audiobook/projects/{project_id}/paragraphs/{paragraph_id}/narrate` (one paragraph) | a narration job |
Chapters are the unit you *organise* by; **paragraphs are the unit that is actually narrated, billed, and re-recorded**. If you only ever need whole-book narration you can stop at step 5 — `POST .../narration/batch` with no `chapter_ids` or `paragraph_ids` narrates the entire project.
## API Endpoints Quick Reference
| Operation | Method | Endpoint |
| - | - | - |
| Account audiobook settings | GET·PATCH | `/audiobook/settings` |
| Create project | POST | `/audiobook/projects` |
| List projects (`page`, `per_page`) | GET | `/audiobook/projects` |
| Get project (`?include_chapters=true`) | GET | `/audiobook/projects/{project_id}` |
| Update / delete project (incl. `multi_voice`) | PUT·DELETE | `/audiobook/projects/{project_id}` |
| Upload manuscript | POST | `/audiobook/projects/{project_id}/manuscript/upload` |
| Parse manuscript | POST | `/audiobook/projects/{project_id}/manuscript/parse` |
| Append another manuscript file | POST | `/audiobook/projects/{project_id}/manuscript/append` |
| Paste text | POST | `/audiobook/projects/{project_id}/manuscript/paste` |
| Generate book | POST | `/audiobook/projects/{project_id}/manuscript/generate` |
| List chapters (`page`, `per_page`) | GET | `/audiobook/projects/{project_id}/chapters` |
| Get / update / delete a chapter | GET·PUT·DELETE | `/audiobook/projects/{project_id}/chapters/{chapter_id}` |
| List paragraphs in a chapter | GET | `/audiobook/projects/{project_id}/chapters/{chapter_id}/paragraphs` |
| Get / update a paragraph | GET·PUT | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}` |
| **Auto Direction — direct the book** | POST | `/audiobook/projects/{project_id}/direction/auto` |
| Latest direction run | GET | `/audiobook/projects/{project_id}/direction/auto/latest` |
| Direction run status | GET | `/audiobook/projects/{project_id}/direction/auto/{job_id}/status` |
| **Cast — read speakers + voices** | GET | `/audiobook/projects/{project_id}/cast` — see [Characters and voices](/api-reference/audiobook-cast) |
| Split lines by speaker (screenplay annotation) | POST | `/audiobook/projects/{project_id}/screenplay/annotate` |
| Annotation status | GET | `/audiobook/projects/{project_id}/screenplay/annotate/{job_id}/status` |
| Detect speakers (`?method=heuristic` instant, or `?method=llm` job) | POST | `/audiobook/projects/{project_id}/cast/detect` |
| Speaker detection job status | GET | `/audiobook/projects/{project_id}/cast/detect/{job_id}/status` |
| Merge an alternate character name | POST | `/audiobook/projects/{project_id}/cast/alias` |
| Assign a voice to a character | POST | `/audiobook/projects/{project_id}/cast/assign` |
| Auto-cast (designs voices — prepaid) | POST | `/audiobook/projects/{project_id}/cast/auto` |
| Auto-cast status | GET | `/audiobook/projects/{project_id}/cast/auto/{job_id}/status` |
| **Pronunciation dictionary — list entries** | GET | `/audiobook/projects/{project_id}/pronunciations` |
| Add a pronunciation entry | POST | `/audiobook/projects/{project_id}/pronunciations` |
| Update a pronunciation entry | PUT | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}` |
| Delete a pronunciation entry | DELETE | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}` |
| List voices | GET | `/audiobook/voices/available` |
| List languages | GET | `/audiobook/voices/languages` |
| Cost estimate | GET | `/audiobook/projects/{project_id}/cost-estimate` |
| Book cost (actual spend) | GET | `/audiobook/projects/{project_id}/cost` |
| **Narrate (batch — the billed path)** | POST | `/audiobook/projects/{project_id}/narration/batch` |
| Narrate a single paragraph | POST | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}/narrate` |
| Narration progress | GET | `/audiobook/projects/{project_id}/narration/progress` |
| Cancel narration | POST | `/audiobook/projects/{project_id}/narration/cancel` |
| Re-render one chapter | POST | `/audiobook/projects/{project_id}/narration/batch` with `chapter_ids` + `include_completed` |
| Retry a chapter's failed paragraphs | POST | `/audiobook/projects/{project_id}/chapters/{chapter_id}/retry-failed-paragraphs` |
| **Narration review — list passages** (`status`) | GET | `/audiobook/projects/{project_id}/review` |
| Approve one passage | POST | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}/review/approve` |
| Approve all passages (optional `chapter_id`) | POST | `/audiobook/projects/{project_id}/review/approve-all` |
| Reject one passage (optional `regenerate`) | POST | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}/review/reject` |
| Production settings — opening/closing announcement, music, cover | GET·PUT | `/audiobook/projects/{project_id}/production-settings` |
| **Pacing** — pauses between paragraphs, speaker changes, scene breaks | GET·PATCH | `/audiobook/projects/{project_id}/pacing` — see [Pacing and scene breaks](#pacing-and-scene-breaks) |
| Upload media (cover/music) | POST | `/audiobook/projects/{project_id}/media/upload` |
| Start ACX export | POST | `/audiobook/projects/{project_id}/export` |
| Export status | GET | `/audiobook/projects/{project_id}/export/{job_id}/status` |
| Download package | GET | `/audiobook/projects/{project_id}/export/{job_id}/download` |
| Dashboard stats | GET | `/audiobook/dashboard` |
`POST /audiobook/projects/{project_id}/narration/start` is **deprecated and removed** — it now returns **`410 Gone`**. Use `POST /audiobook/projects/{project_id}/narration/batch`: with no `chapter_ids` and no `paragraph_ids` it narrates the whole project, which is exactly what `narration/start` used to do.
`POST /audiobook/projects/{project_id}/chapters/{chapter_id}/regenerate` is **deprecated and removed** and also returns **`410 Gone`**. Use `POST /audiobook/projects/{project_id}/narration/batch` with `{"chapter_ids": [""], "include_completed": true}` — `include_completed` is required, because a chapter you are RE-rendering already has audio and would otherwise be skipped entirely.
## Quick Start (cURL)
```bash theme={null}
export AUDIOPOD_API_KEY="ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export AP="https://api.audiopod.ai/api/v1"
# 1. Create a project
PID=$(curl -s -X POST "$AP/audiobook/projects" -H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"The Lighthouse Keeper","author":"A. P. Tester"}' | jq -r .id)
# 2. Upload a manuscript (PDF / EPUB / DOCX / TXT)
FILE_KEY=$(curl -s -X POST "$AP/audiobook/projects/$PID/manuscript/upload" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -F "file=@book.epub" | jq -r .file_key)
# 3. Parse into chapters (async). API keys default to auto_direction=false:
# pass auto_direction=true to have every paragraph directed after parsing (free)
curl -s -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -F "file_key=$FILE_KEY" -F "filename=book.epub" \
-F "auto_direction=true"
# 3b. Poll the PROJECT until parsing finishes — status tells you which way it went
while :; do
P=$(curl -s "$AP/audiobook/projects/$PID?include_chapters=true" -H "X-API-Key: $AUDIOPOD_API_KEY")
S=$(echo "$P" | jq -r .status)
[ "$S" = "failed" ] && { echo "parse failed: $(echo "$P" | jq -r .error_message)"; exit 1; }
[ "$(echo "$P" | jq -r .parsing_completed_at)" != "null" ] && break
sleep 5
done
echo "$P" | jq '.chapters[] | {id, chapter_number, title}'
# 4. Narrate the whole project with a voice (omit chapter_ids/paragraph_ids = everything)
curl -s -X POST "$AP/audiobook/projects/$PID/narration/batch" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"voice_id":387}'
# 4b. Handle passages that need review (see "Narration review" below).
# When narration settles, needs_review_paragraphs > 0 means some passages
# wait for a decision and export will refuse until they are resolved.
curl -s "$AP/audiobook/projects/$PID/narration/progress" -H "X-API-Key: $AUDIOPOD_API_KEY" \
| jq .needs_review_paragraphs
curl -s "$AP/audiobook/projects/$PID/review" -H "X-API-Key: $AUDIOPOD_API_KEY" \
| jq '.items[] | {paragraph_id, expected_text, heard_text, candidate_audio_url}'
# Listen, then approve them all (free) — or approve / reject one at a time
curl -s -X POST "$AP/audiobook/projects/$PID/review/approve-all" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "approved": 2, "refused": 0, "remaining": 0, "refused_reasons": {} }
# Fully automated pipeline? Create the project with "narration_review_policy":"auto_accept"
# instead, and nothing ever waits for review.
# 5. Export an ACX package, then download it
JOB=$(curl -s -X POST "$AP/audiobook/projects/$PID/export" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}' | jq -r .job_id)
curl -s "$AP/audiobook/projects/$PID/export/$JOB/download" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "download_url": "https://media.audiopod.ai/...zip?...", "file_size_bytes": ..., "chapter_count": ... }
```
## 1. Create a project
A project is the container for a single audiobook. Create it first, then attach a manuscript.
Audiobook title.
Author name (written into the package metadata).
Project description.
ISBN, if available.
Book genre.
Default narration voice for the project (can also be set per narration request).
Speech speed multiplier.
Narration style.
Export format. `acx` produces an Audible/ACX-ready package.
Target distribution platform.
What happens to a passage our narration check cannot confirm after its retry: `manual` holds it for your review (the book waits on it); `auto_accept` keeps the best render and flags it, so the book never blocks. See [Narration review](#narration-review). Also accepted on `PUT /audiobook/projects/{project_id}` (`null` leaves the stored value unchanged).
```bash theme={null}
curl -X POST "$AP/audiobook/projects" -H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"The Lighthouse Keeper","author":"A. P. Tester","genre":"Fiction"}'
```
```python theme={null}
import requests
AP, H = "https://api.audiopod.ai/api/v1", {"X-API-Key": "ap_..."}
project = requests.post(f"{AP}/audiobook/projects", headers=H, json={
"title": "The Lighthouse Keeper", "author": "A. P. Tester", "genre": "Fiction",
}).json()
pid = project["id"]
```
**Response** (`AudiobookProjectResponse`):
Project UUID — **this is your `project_id`** for every subsequent call.
Lifecycle state (see [Status values](#status-values)).
Number of chapters once parsed. This is a *count* — the chapter objects are not in this response unless you ask for them.
Estimated finished runtime.
Uploaded filename, once attached.
`pdf` · `epub` · `docx` · `txt`.
Total words after parsing.
ISO timestamp set when a manuscript parse finished successfully; `null` until then.
ISO timestamp of the first narration job.
ISO timestamp when narration finished.
ISO timestamp when production/export finished.
Sanitized failure reason when a stage failed; `null` otherwise.
Project default narration voice.
Speed multiplier (0.5–2.0).
Delivery style.
Export format (`acx`).
Target distribution platform.
`manual` or `auto_accept` — see [Narration review](#narration-review).
Creation timestamp.
Last-modified timestamp.
### Read one project
`GET /audiobook/projects/{project_id}` returns the same body, plus a `chapters` field:
When `true`, the response carries the project's full `chapters` array inline, saving a second round-trip. When omitted or `false`, `chapters` is `null` — the chapter objects (and their `id`s) are simply not there.
```bash theme={null}
curl -s "$AP/audiobook/projects/$PID?include_chapters=true" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "id": "...", "status": "ready_for_narration", "total_chapters": 12,
# "parsing_completed_at": "2026-07-23T09:14:02", "error_message": null,
# "chapters": [ { "id": "...", "chapter_number": 1, "title": "...", ... } ] }
```
### List projects
`GET /audiobook/projects` is paginated with **`page`** and **`per_page`** (not `skip`/`limit`):
1-based page number (minimum `1`).
Projects per page, `1`–`100`.
Return only projects in this [status](#status-values).
Case-insensitive substring match on title or author.
```bash theme={null}
curl -s "$AP/audiobook/projects?page=2&per_page=50" -H "X-API-Key: $AUDIOPOD_API_KEY"
```
The page of projects (each an `AudiobookProjectResponse`).
Total projects matching the filters, across all pages.
Echo of the requested page.
Echo of the requested page size.
Whether another page follows.
Whether a previous page exists.
Unknown query parameters are **ignored, not rejected**. Sending `?skip=0&limit=20` returns a `200` with the *first* page every time — so a paging loop built on `skip`/`limit` never advances and silently re-reads page 1 forever. Use `page` / `per_page`, and drive your loop off `has_next`.
## 2. Add the manuscript
Three ways to get text into a project — pick one.
### Upload a file
`POST /audiobook/projects/{project_id}/manuscript/upload` — multipart form field `file` (PDF, EPUB, DOCX, or TXT). Returns a `file_key` to hand to the parser. Optional form field `multi_voice` (`true`/`false`) saves the [character voices](/api-reference/audiobook-cast) choice on the book, as with `PUT /audiobook/projects/{project_id}`; the parse that follows then detects the speakers and gives each character a free library voice.
Storage key of the uploaded manuscript — pass this to the parser.
Reference URL for the stored file.
Maximum accepted size in bytes.
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/manuscript/upload" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -F "file=@book.epub"
```
### Parse the uploaded file into chapters
`POST /audiobook/projects/{project_id}/manuscript/parse` — pass `file_key` (from the upload response) and `filename`. The endpoint accepts a **JSON body** or **form fields** (`multipart/form-data` or `application/x-www-form-urlencoded`) — use whichever your HTTP client makes easiest. Parsing is asynchronous and returns a `job_id` immediately — see [Poll for parse completion](#poll-for-parse-completion) for how to wait on it.
```bash theme={null}
# JSON body
curl -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d "{\"file_key\": \"$FILE_KEY\", \"filename\": \"book.epub\"}"
# ...or form fields (equivalent)
curl -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -F "file_key=$FILE_KEY" -F "filename=book.epub"
# => { "job_id": 99, "estimated_completion_time": "under 2 minutes" }
```
Storage key returned by the upload response.
Original filename, including the extension.
Re-parse a project that already has chapters. **Destructive** — existing chapters and paragraphs are replaced.
Run [Auto Direction](#auto-direction) as soon as parsing finishes, so the book narrates expressively without a second call. **Defaults to `false` for API-key callers** — see below.
Attribute dialogue to named speakers (each paragraph's `character_name`). Free. Omitted: **off for API-key requests**, on in the web studio. `multi_voice: true` turns it on. See [Characters and voices](/api-reference/audiobook-cast#finding-the-speakers).
Give each character their own voice. Turns on `detect_speakers`, and every character found gets a library voice at no cost when a suitable one is available, which you can change on the cast. After parsing, our speaker detection pass reads the dialogue in context, merges alternate names of the same character, and records a gender hint used for the free voice pick; narration of a passage waits until it finishes, and Auto Direction, when requested, runs after it. See [the speaker detection pass](/api-reference/audiobook-cast#speaker-detection-pass-with-character-voices-on). Omitted: the book's setting is left as it is (new books start with it off: every line uses the narrator). See [Characters and voices](/api-reference/audiobook-cast).
**API-key parses default to `auto_direction=false`.** Set `auto_direction=true` explicitly to add reading direction after parsing. The account preference does not change this API-key default.
In Audiobook Studio, an omitted `auto_direction` follows the author's [account preference](#account-audiobook-settings) when that setting is active. An explicit `true` or `false` overrides the default for that parse, for either authentication method. Explicit `true` also works when the account preference is not active.
You can also direct a book at any time after parsing with [`POST .../direction/auto`](#auto-direction) — nothing about it is tied to parse time.
```bash theme={null}
# Parse AND direct in one call
curl -X POST "$AP/audiobook/projects/$PID/manuscript/parse" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d "{\"file_key\": \"$FILE_KEY\", \"filename\": \"book.epub\", \"auto_direction\": true}"
```
### Poll for parse completion
There is **no parse-status endpoint**, and polling `GET .../chapters` "until chapters appear" is not a safe wait — a *failed* parse produces no chapters, so that loop never terminates. Poll the **project** instead: it is the object that carries the parse outcome.
Poll `GET /audiobook/projects/{project_id}?include_chapters=true` — one call returns both the parse state and, on success, the chapters — and branch on these three fields:
`parsing` while the job is running. On success it becomes `ready_for_narration`. On a failed **first** parse it becomes `failed`.
`null` until a parse has completed successfully; an ISO timestamp afterwards. **This is the success signal.**
The sanitized failure reason, set when the parse fails (e.g. an unreadable or over-length manuscript). Read it to report *why* to your user.
Decision table — evaluate in this order:
| Condition | Meaning | Do |
| - | - | - |
| `status == "failed"` | **Terminal failure.** | Stop. Surface `error_message`; fix or re-upload the manuscript. |
| `parsing_completed_at != null` | **Success.** | Stop. Read `chapters` (or call the chapters endpoint). |
| `status == "parsing"` | Still working. | Sleep and poll again. |
| Neither, after your timeout | Treat as failed. | Give up after a bounded number of attempts. |
Check `status` **before** `error_message`. `error_message` is not cleared when you retry a parse, so a leftover value from an earlier failure can persist on a project that has since parsed fine. `status` (plus `parsing_completed_at`) is the authoritative state; `error_message` is the *reason* to display once `status` says `failed`.
One nuance for `manuscript/append`: if an append-parse fails on a project that **already** has chapters, the project is returned to `ready_for_narration` rather than `failed`, so the existing book is not destroyed. In that case compare `total_chapters` before and after to detect that nothing was added.
```python theme={null}
import time, requests
AP, H = "https://api.audiopod.ai/api/v1", {"X-API-Key": "ap_..."}
def wait_for_parse(pid: str, timeout_s: int = 900, interval_s: int = 5) -> list[dict]:
deadline = time.time() + timeout_s
while time.time() < deadline:
p = requests.get(f"{AP}/audiobook/projects/{pid}",
params={"include_chapters": "true"}, headers=H).json()
if p["status"] == "failed": # terminal failure
raise RuntimeError(p.get("error_message") or "Manuscript parsing failed")
if p.get("parsing_completed_at"): # terminal success
return p["chapters"] or []
time.sleep(interval_s) # still parsing
raise TimeoutError("Manuscript parsing did not finish in time")
chapters = wait_for_parse(pid)
chapter_ids = [c["id"] for c in chapters]
```
Parses typically finish in under two minutes for EPUB/DOCX/TXT and in 2–10 minutes for large or scanned PDFs; the `estimated_completion_time` string in the parse response reflects the format you submitted. A 15-minute poll ceiling is a safe default.
### Paste text directly
`POST /audiobook/projects/{project_id}/manuscript/paste` — JSON. Best for content you already have as text.
Manuscript or chapter text.
Source label.
Add automatic reading direction after splitting the text into chapters. Omitted or `null` follows the [account preference](#account-audiobook-settings) for Studio requests and defaults to `false` for API keys. An explicit `true` or `false` overrides that default for this import.
Attribute dialogue to named speakers (each paragraph's `character_name`). Free. Omitted: **off for API-key requests**, on in the web studio. `multi_voice: true` turns it on. See [Characters and voices](/api-reference/audiobook-cast#finding-the-speakers).
Give each character their own voice. Turns on `detect_speakers`, and every character found gets a library voice at no cost when a suitable one is available, which you can change on the cast. After parsing, our speaker detection pass reads the dialogue in context, merges alternate names of the same character, and records a gender hint used for the free voice pick; narration of a passage waits until it finishes, and Auto Direction, when requested, runs after it. See [the speaker detection pass](/api-reference/audiobook-cast#speaker-detection-pass-with-character-voices-on). Omitted: the book's setting is left as it is (new books start with it off: every line uses the narrator). See [Characters and voices](/api-reference/audiobook-cast).
```bash theme={null}
# Paste text and direct it in one call (API keys must ask for direction explicitly)
curl -X POST "$AP/audiobook/projects/$PID/manuscript/paste" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"text": "Capitolo 1\n\nC'"'"'era una volta un pezzo di legno...", "auto_direction": true}'
# => { "project_id": "...", "source_type": "paste", "total_chapters": 2, "total_words": 1344,
# "estimated_duration_minutes": 8.96, "requires_author_approval": true }
```
### Generate a book from a prompt
`POST /audiobook/projects/{project_id}/manuscript/generate` — JSON. Drafts an original manuscript, then chapters it.
What the book should be about.
Optional working title.
Intended audience.
Writing tone.
Number of chapters.
Target words per chapter.
Add automatic reading direction after generating and splitting the manuscript. Omitted or `null` follows the [account preference](#account-audiobook-settings) for Studio requests and defaults to `false` for API keys. An explicit `true` or `false` overrides that default for this import.
Attribute dialogue to named speakers (each paragraph's `character_name`). Free. Omitted: **off for API-key requests**, on in the web studio. `multi_voice: true` turns it on. See [Characters and voices](/api-reference/audiobook-cast#finding-the-speakers).
Give each character their own voice. Turns on `detect_speakers`, and every character found gets a library voice at no cost when a suitable one is available, which you can change on the cast. After parsing, our speaker detection pass reads the dialogue in context, merges alternate names of the same character, and records a gender hint used for the free voice pick; narration of a passage waits until it finishes, and Auto Direction, when requested, runs after it. See [the speaker detection pass](/api-reference/audiobook-cast#speaker-detection-pass-with-character-voices-on). Omitted: the book's setting is left as it is (new books start with it off: every line uses the narrator). See [Characters and voices](/api-reference/audiobook-cast).
The `paste` and `generate` endpoints return a `ManuscriptIntakeResponse` with `total_chapters`, `total_words`, and `estimated_duration_minutes`.
When automatic direction is requested, it continues asynchronously after the manuscript is saved. Read `GET /audiobook/projects/{project_id}/direction/auto/latest` to find its job and follow [direction progress](#waiting-for-a-direction-run). Direction does not start audio narration; submit a narration request when you are ready.
`manuscript/parse`, `manuscript/paste`, and `manuscript/generate` **replace the project's entire chapter/paragraph structure** — any existing narration is discarded. Parse once per manuscript; re-parse only if the manuscript file itself changed.
While narration, another parse, mixing, or an export is already running for the project, these endpoints return `409 Conflict` — poll `GET .../narration/progress` and retry once it finishes. If you don't want to wait for a narration run to finish, stop it first with [`POST .../narration/cancel`](#cancel-a-narration-run); already-narrated paragraphs are kept.
If the project already has completed narration, these endpoints instead return `409` with a structured body: `detail.code = "completed_narration_exists"` and `detail.completed_paragraphs = N`. To proceed anyway, pass `force=true` (accepted as a JSON field, form field, or query parameter). **Do not hard-code `force=true` in your integration** — treat this `409` as a stop-and-confirm; forcing permanently deletes the `N` narrated paragraphs.
`manuscript/append` is the exception: it's additive (appends chapters after the existing ones) and never deletes narration.
## 3. Chapters
List chapters with `GET /audiobook/projects/{project_id}/chapters`, or fetch one with `GET .../chapters/{chapter_id}`.
1-based page number (minimum `1`).
Chapters per page, `1`–`500`.
The response is the same paginated envelope as the project list — `chapters`, `total`, `page`, `per_page`, `has_next`, `has_prev` — with chapters ordered by `chapter_number`. As with projects, `skip`/`limit` are **not** recognised and are silently ignored.
```bash theme={null}
curl -s "$AP/audiobook/projects/$PID/chapters?page=1&per_page=100" -H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Chapter** (`AudiobookChapterResponse`):
Chapter UUID — **this is your `chapter_id`**.
Back-reference to the owning project.
Order in the book.
Chapter title.
Chapter text.
Words in the chapter.
Characters in the chapter.
Narrated length, once generated.
`pending` · `queued` · `narrating` · `completed` · `failed`.
Stored audio key, once narrated.
Reason a chapter failed (e.g. content too short for narration).
If true, the chapter is excluded from narration and export.
Per-chapter voice override.
Free-text narration instructions stored on the chapter.
Estimated narrated length before rendering.
Number of narration retries so far.
You can edit a chapter (title, content, `skip_narration`, per-chapter `voice_id`, narration notes) with `PUT .../chapters/{chapter_id}` before narrating.
## 3b. Paragraphs
Chapters are containers; **paragraphs are the atomic unit of narration**. Each paragraph is narrated, billed, re-recorded, and version-tracked (as "takes") independently, so paragraph-level access is what you want for fine-grained control — fixing one mispronounced sentence without re-rendering a 40-minute chapter.
### List a chapter's paragraphs
`GET /audiobook/projects/{project_id}/chapters/{chapter_id}/paragraphs` — returned in `position` order.
1-based page number. **Optional.**
Paragraphs per page, `1`–`200`. **Optional**; defaults to 50 when only `page` is given.
Pagination here is **opt-in**: omit *both* `page` and `per_page` and you get every paragraph in the chapter in a single response. Supply either one and paging switches on. `total` is always the chapter's full paragraph count, never the length of the page you received.
**Response** (`PaginatedParagraphsResponse`):
The paragraphs, ordered by `position`.
Total paragraphs in the chapter.
Echo of the requested page; `null` in unpaginated mode.
Echo of the requested page size; `null` in unpaginated mode.
Whether another page follows (`false` in unpaginated mode).
Whether a previous page exists (`false` in unpaginated mode).
**Paragraph** (`AudiobookParagraphResponse`):
Paragraph UUID — **this is your `paragraph_id`**.
Back-reference to the owning chapter.
Back-reference to the owning project.
Order within the chapter (the sort key).
The text that will be narrated.
Words in the paragraph.
Characters in the paragraph (narration is metered per character).
Per-paragraph voice override, if set.
Per-paragraph speed override.
Per-paragraph delivery style.
Emotional direction for this paragraph.
Speaking character, when the book is cast.
Screenplay-style direction applied to this line.
Silence appended after this paragraph, in milliseconds (0–10000). `null` follows the book's [pacing](#pacing-and-scene-breaks); a value here always wins.
Stored audio key, once narrated.
Narrated length, once generated.
`pending` · `queued` · `narrating` · `completed` · `failed`.
A locked paragraph is protected: edits and regeneration are refused with `409` until it is unlocked.
If true, the paragraph is excluded from narration and export.
Sanitized reason this paragraph failed.
Narration retries so far.
```bash theme={null}
# every paragraph in the chapter
curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs" \
-H "X-API-Key: $AUDIOPOD_API_KEY" | jq '.paragraphs[] | {id, position, status}'
# or a page at a time
curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs?page=1&per_page=50" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
Read one paragraph with `GET .../projects/{project_id}/paragraphs/{paragraph_id}` (handy for status polling) and edit one with `PUT .../projects/{project_id}/paragraphs/{paragraph_id}`.
### Narrate a single paragraph
`POST /audiobook/projects/{project_id}/paragraphs/{paragraph_id}/narrate` generates — or regenerates — audio for exactly one paragraph and records a new *take*. The body is optional; send `{}` to narrate with the paragraph's existing settings.
Voice for this take; falls back to the paragraph's, then the project's, voice.
Narrate this text instead of the stored paragraph text (the stored text is unchanged).
Speed multiplier for this take, `0.5`–`2.0`.
Delivery style for this take.
Emotional direction for this take.
Speaking character (max 120 chars).
Performance direction for this take; takes precedence over `narration_style`. The field accepts 500 characters but only the first **110** are performed — see [Auto Direction](#auto-direction) for how to write one, and let it write them for you.
**Response:**
Background task identifier for this narration.
The paragraph being narrated.
`queued` — the paragraph's status is set to `queued` immediately.
`is_free_regen`, `estimated_credits`, `estimated_chars`, `estimated_seconds`, `take_number_after`.
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID/narrate" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"voice_id":387,"emotion":"wistful"}'
# => { "task_id": "...", "paragraph_id": "...", "status": "queued",
# "billing": { "is_free_regen": true, "estimated_credits": 0, "take_number_after": 2 } }
```
Poll `GET .../projects/{project_id}/paragraphs/{paragraph_id}` until `status` is `completed` (or `failed`, with `error_message`). The first take of a paragraph is billed, the **first regeneration is free**, and subsequent regenerations are billed again — `billing.is_free_regen` tells you which case you are in *before* the work runs. A `409` means the paragraph is locked; unlock it with `PUT .../paragraphs/{paragraph_id}` first.
### Pacing and scene breaks
How long the finished book pauses between passages is a **book setting**, not something you set line by line. Three values, each in milliseconds (`0`–`10000`):
| Field | Default | The pause… |
| - | - | - |
| `paragraph_gap_ms` | `500` | between two passages read by the same voice. When a named character speaks two lines in a row, the gap is half this value. |
| `speaker_change_gap_ms` | `500` | when the voice changes — narrator to a character, or one character to another. |
| `scene_break_ms` | `1500` | at a scene break. |
A paragraph's own `pause_after_ms` always wins for the gap after that paragraph.
**Scene breaks are detected for you.** A paragraph made only of divider marks — `* * *`, `***`, `#`, `###`, `⁂`, `~~~`, `---`, `—`, `•••`, `o0o`, a centred `◆`, `❖` or `§` — is a scene break. It stays in the chapter's paragraph list (so you can see where it is), comes back with `skip_narration: true`, is **never narrated and never charged**, and becomes a `scene_break_ms` pause in the chapter audio. Text that merely contains those marks (`***bold***`) or a heading (`# Chapter 1`) is ordinary text. A divider at the very start or end of a chapter is dropped — there is no scene on the other side of it.
Pause between passages by the same voice, `0`–`10000` ms.
Pause when the voice changes, `0`–`10000` ms.
Pause at a scene break, `0`–`10000` ms.
Send only the fields you want to change; the response is the full, current pacing.
```bash theme={null}
# read the book's pacing (a book that never set it returns the defaults)
curl -s "$AP/audiobook/projects/$PID/pacing" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "paragraph_gap_ms": 500, "speaker_change_gap_ms": 500, "scene_break_ms": 1500 }
# more air between voices and a longer pause at scene breaks
curl -X PATCH "$AP/audiobook/projects/$PID/pacing" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"speaker_change_gap_ms": 900, "scene_break_ms": 2500}'
# => { "paragraph_gap_ms": 500, "speaker_change_gap_ms": 900, "scene_break_ms": 2500 }
```
Pacing is applied when chapters are assembled, so changing it **never re-narrates anything and costs nothing**. Assembled chapter audio is refreshed the next time it is played or downloaded, and every export uses the current pacing. Saving pacing also marks divider-only paragraphs in a book imported before scene breaks were recognised, so you don't have to import it again.
## 3c. Auto Direction
A narrator that reads every line the same way sounds flat, however good the voice is. **Direction** is what fixes that: a short performance note on each paragraph — how the line is delivered, and with what feeling — that the narrator acts on.
You can write directions yourself, one paragraph at a time (see [Direct a single line](#direct-a-single-line)). Auto Direction writes them for the whole book in one call.
`POST /audiobook/projects/{project_id}/direction/auto`
It runs in two passes. First a **narration brief** for the book as a whole — its reading identity, derived from the metadata and the opening pages. Then a performance note and an emotion per paragraph, each anchored to that brief, so the tone holds across a long book instead of drifting chapter to chapter. Dialogue is directed as dialogue.
Direct only these chapters. Omit to direct the whole book.
```bash theme={null}
# Direct the whole book
curl -X POST "$AP/audiobook/projects/$PID/direction/auto" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}'
# => { "job_id": "901", "total_chapters": 23 }
# ...or one chapter
curl -X POST "$AP/audiobook/projects/$PID/direction/auto" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d "{\"chapter_ids\": [\"$CHAPTER_ID\"]}"
```
**Auto Direction is free on every plan**, including the free tier. It is the same class of work as chapter detection, and a book that only sounds good on a paid plan is a worse product.
### Waiting for a direction run
```bash theme={null}
# Status of a specific run...
curl "$AP/audiobook/projects/$PID/direction/auto/901/status" -H "X-API-Key: $AUDIOPOD_API_KEY"
# ...or of the most recent one, when you did not keep the job_id
curl "$AP/audiobook/projects/$PID/direction/auto/latest" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "job_id": "901", "status": "COMPLETED", "progress": 100,
# "result": { "directed_count": 315, "skipped_user_edited": 4,
# "chapters_processed": 23, "total_chapters": 23 } }
```
Poll until `status` is `COMPLETED` or `FAILED`. A `409` on the POST means a run is already in flight for this project.
**Directing does not re-render audio.** It only writes `direction` and `emotion` onto paragraphs that already exist — it never re-splits chapters and never deletes a take. Paragraphs already narrated keep the audio they have until you narrate them again:
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d "{\"voice_id\": 389, \"chapter_ids\": [\"$CHAPTER_ID\"], \"include_completed\": true}"
```
Re-narration is billed normally, so try one chapter before committing a whole book.
### Your directions win
A direction you wrote by hand is **never overwritten** by a later automatic run. Writing one via `PUT .../paragraphs/{paragraph_id}` marks that row as yours, and every subsequent Auto Direction skips it — the run reports how many it left alone in `result.skipped_user_edited`.
The suggestion it replaced is kept on the row, so reverting to the automatic version costs nothing and needs no new run.
### Direct a single line
`PUT /audiobook/projects/{project_id}/paragraphs/{paragraph_id}`
```bash theme={null}
curl -X PUT "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"direction": "urgent, low, barely audible", "emotion": "tense"}'
```
A short stage note the narrator acts on. See the length note below.
One of `neutral`, `warm`, `tense`, `sad`, `excited`, `intimate`, `dramatic`, `authoritative`. `neutral` means "no particular feeling".
**Write a direction short.** The field accepts up to 500 characters, but only the first **110** shape the performance — a longer note is trimmed at a word boundary and the rest is not voiced. One instruction of roughly a dozen words works best: `"urgent, low, barely audible"`, not a paragraph of stage notes.
When you send a longer one, the response carries a `warnings` entry telling you exactly how much of it will not be heard.
## Characters and voices
A book with dialogue can give every character their own voice. Character voices are off by default; send `multi_voice: true` when you add the manuscript (or on `PUT /audiobook/projects/{project_id}`) and every character found gets a library voice for free (when a suitable one is available). Then review the cast, merge alternate names and change any voice before you narrate. The full workflow, with every endpoint, is on **[Characters and voices (cast)](/api-reference/audiobook-cast)**.
## 3d. Pronunciation dictionary
Some words need a nudge no voice selection can give you — the wrong stress on a place name, a word from another language read as if it were English. A **pronunciation dictionary** fixes this per project: map a written word (`source`) to what the narrator should actually say (`alias`), and every future narration or regeneration in every chapter of the project picks it up automatically.
The substitution happens only at synthesis time. The manuscript, the paragraph text returned by the API, and everything in the exported package all keep the original spelling — only what gets spoken changes.
### Manage entries
Same `X-API-Key` auth as the rest of this API.
| Operation | Method | Endpoint |
| - | - | - |
| List entries | GET | `/audiobook/projects/{project_id}/pronunciations` |
| Add an entry | POST | `/audiobook/projects/{project_id}/pronunciations` |
| Update an entry | PUT | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}` |
| Delete an entry | DELETE | `/audiobook/projects/{project_id}/pronunciations/{pronunciation_id}` |
The written word or phrase to match, 1–200 characters.
What the narrator says instead of `source`, minimum 1 character. See [Writing a good alias](#writing-a-good-alias) below.
When `false`, `source` matches regardless of case.
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/pronunciations" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"source": "Otranto", "alias": "Òtranto"}'
# => 201 { "id": "...", "source": "Otranto", "alias": "Òtranto", "case_sensitive": false }
```
`POST` returns `201` on success and `409` when `source` already has an entry for this project — update the existing one instead of retrying the create:
```bash theme={null}
curl -X PUT "$AP/audiobook/projects/$PID/pronunciations/$PRONUNCIATION_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"alias": "Òtranto", "case_sensitive": true}'
curl -X DELETE "$AP/audiobook/projects/$PID/pronunciations/$PRONUNCIATION_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
# => 204
```
### Matching semantics
* **Whole-word.** A match's boundaries are non-alphanumeric characters or the start/end of the text, so `source: "rise"` matches the standalone word `rise` and not the `rise` inside `surprise`.
* **Multi-word sources work**, including hyphens, apostrophes, and periods — `"Mr. Smith"` is a valid `source`.
* **Case-insensitive by default.** Set `case_sensitive: true` to require an exact-case match.
* **Longer sources win.** If two entries' sources overlap on the same text, the longer one is applied.
### Writing a good alias
The safe default, and what works on **every** voice: an accented or phonetic respelling of the same word. It doesn't need to be true IPA — anything that nudges the reader to the right sound works:
| Problem | `source` | `alias` |
| - | - | - |
| Wrong stress on a place name | `Otranto` | `Òtranto` (or `Oh-TRAHN-toh`) |
| A word read in the wrong language | `rise` | `rìse` |
**IPA is advanced, and voice-dependent.** `alias` may instead be a single word's IPA transcription wrapped in forward slashes — one word per pair of slashes, no spaces inside: `/ˈɔtranto/`. This is honored only on voices that support expressive delivery (the [`delivery_mode`](#delivery-mode-how-performed-the-read-is) setting). On a voice without that support, the slash-wrapped notation is stripped before synthesis and **the word is silently dropped from the narration** — not mispronounced, missing. Unless you know the voice you're narrating with supports it, use an accented or phonetic respelling instead; it's honored on every voice.
Pronunciation fixes belong here, in `alias` (or by editing the paragraph text itself) — not in a paragraph's `direction` field. `direction` shapes performance, not pronunciation, and phonetic notation placed there is stripped rather than spoken.
### Applying a fix without re-rendering the whole book
New entries apply automatically to narration going forward, but a paragraph that was already narrated keeps its existing audio until it is re-rendered. `POST .../narration/batch` accepts `paragraph_ids` (in addition to `chapter_ids`), so you can re-render only the affected lines instead of a whole chapter — get the ids from [`GET .../chapters/{chapter_id}/paragraphs`](#list-a-chapters-paragraphs):
```bash theme={null}
# find the paragraphs that mention the word you just fixed
curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs" \
-H "X-API-Key: $AUDIOPOD_API_KEY" | jq -r '.paragraphs[] | select(.text | test("Otranto")) | .id'
# re-render only those paragraphs
curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d "{\"voice_id\": 387, \"paragraph_ids\": [\"$PARA_ID\"], \"include_completed\": true}"
```
Narration billing is per paragraph: the first take is billed, the first regeneration of a paragraph is free, and later regenerations bill again at the normal rate — the same rule as [manual regeneration](#narrate-a-single-paragraph). `GET /audiobook/projects/{project_id}/cost-estimate` already reflects this per paragraph, so you can check what a targeted re-render will actually cost before you run it.
## 4. Choose a voice
`GET /audiobook/voices/available` returns a `VoiceSelectionResponse`:
All standard voices you can narrate with (each has an integer `id`).
A curated shortlist.
Your own cloned voices (see [Voice Management](/api-reference/voice-management)).
`GET /audiobook/voices/languages` lists every supported narration locale (100+). Before committing, `GET /audiobook/projects/{project_id}/cost-estimate` returns a per-paragraph breakdown with `word_count`, `estimated_seconds`, and `credits_to_charge`.
### Book cost
`GET /audiobook/projects/{project_id}/cost` returns what narrating this book has **actually** cost so far. Unlike the cost estimate, it is read from your billing records: every charge taken for a passage of this book, including regenerations. Narration is prepaid, so a charge is taken when a passage is queued and returned if that passage fails or is cancelled. Narration is priced at **\$0.09 per 1,000 characters** (see [Pricing](#pricing)).
```bash theme={null}
curl -s "https://api.audiopod.ai/api/v1/audiobook/projects/$PID/cost" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
```json theme={null}
{
"project_id": "3f6c2a1e-8b4d-4c1a-9e2f-5d7b9a0c1e23",
"currency": "USD",
"billing_lane": "api_wallet",
"totals": {
"charged_cents": 4770,
"refunded_cents": 90,
"net_cents": 4680,
"charged_credits": 0,
"refunded_credits": 0,
"net_credits": 0,
"characters_billed": 520000
},
"breakdown": {
"first_narration": { "characters": 500000, "cents": 4500, "credits": 0 },
"regenerations": { "characters": 20000, "cents": 180, "credits": 0, "count": 14 },
"free_regenerations": 37,
"review_approvals": 6
},
"chapters": [
{ "chapter_id": "a1b2c3d4-0000-4000-8000-000000000001", "chapter_number": 1, "title": "Chapter One", "characters": 260000, "cents": 2340, "credits": 0 },
{ "chapter_id": "a1b2c3d4-0000-4000-8000-000000000002", "chapter_number": 2, "title": "Chapter Two", "characters": 260000, "cents": 2340, "credits": 0 }
],
"partial": false,
"notes": [],
"as_of": "2026-10-09T12:00:00Z"
}
```
**First narration vs regenerations.** `first_narration` is each passage's first take. When you edit text and re-render, only the edited passages are re-narrated, not the whole chapter. The first regeneration of each passage is free (counted in `free_regenerations`); every take after that is a paid regeneration (`regenerations`). Approving a flagged narration from [review](#narration-review) costs nothing (counted in `review_approvals`).
The book.
`"USD"`. Every `*_cents` value is in US cents.
`"api_wallet"` (paid from the API wallet), `"credits"` (paid from account credits) or `"mixed"` (both).
`charged_cents` / `charged_credits`: every charge taken for the book, including ones later returned. `refunded_cents` / `refunded_credits`: charges returned for passages that failed or were cancelled. `net_cents` / `net_credits`: `charged - refunded`, what the book cost; `net_cents` equals the sum of the API wallet debits for this book. `characters_billed`: billable characters of the narration kept.
`first_narration`: `{characters, cents, credits}` for each passage's first narration. `regenerations`: `{characters, cents, credits, count}` for paid regenerations (every take after a passage's first). `free_regenerations`: count of regenerations that cost nothing (a passage's first regeneration is free). `review_approvals`: count of flagged narrations approved from review, which cost nothing.
Per chapter, `{chapter_id, chapter_number, title, characters, cents, credits}` (net). Passages deleted since they were narrated stay counted under their chapter.
`true` when older charges made while this book was being narrated can no longer be tied to one of its passages; they are left out of the figures and `notes` says how many. `false` means every charge is accounted for.
Explanations, empty when there is nothing to add.
When these figures were read (UTC, ISO 8601).
Returns `404` if the book does not exist or is not yours.
## 5. Narrate
### Narrate the book — `narration/batch`
`POST /audiobook/projects/{project_id}/narration/batch` is **the** narration endpoint. It queues one job that narrates the selected paragraphs in order. Scope is chosen by what you send:
* **omit both `chapter_ids` and `paragraph_ids`** → narrate the entire project;
* **`chapter_ids`** → narrate only those chapters;
* **`paragraph_ids`** → narrate only those paragraphs.
Voice to narrate with (from `voices/available`). Optional — a narrator for this run, in place of the book's. With character voices off it voices every selected paragraph. With [character voices](/api-reference/audiobook-cast) on (`multi_voice`) it voices only the narrator's lines: characters keep their cast voice, and a paragraph given its own voice keeps it. It is a narrator override: it is saved on the narrator lines it voices, like any narration voice, and with character voices off it replaces the voice a line had, including one set with a `PUT`. Omit it to use the [voice order](/api-reference/audiobook-cast#voice-order).
Optional — turn character voices on or off, saved on the book before this run reads its voices. `true` also gives uncast characters a library voice.
Optional — UUIDs of chapters to narrate.
Optional — UUIDs of individual paragraphs to narrate.
Omit `chapter_ids` and `paragraph_ids` to narrate the whole book. An **empty** list (`[]`) is refused with `400` and `"code": "empty_selection"`, so a filter of yours that matched nothing never narrates, or bills, the whole book.
Re-narrate paragraphs that already have finished audio.
Include locked paragraphs.
Include paragraphs marked `skip_narration`.
Per-run narration settings — see [Delivery mode](#delivery-mode-how-performed-the-read-is) below.
Optional — `manual` or `auto_accept`. When sent, it is saved on the project for this and later narration. See [Narration review](#narration-review).
#### Delivery mode — how performed the read is
`narration_settings.delivery_mode` is the single biggest lever on how a book *sounds*:
| Value | Read |
| - | - |
| `stable` | One steady narrator from the first paragraph to the last. Least performance range. **This is the default for book narration.** |
| `balanced` | Between the two. |
| `creative` | The most range and variation — usually what narrative fiction wants. |
Books default to `stable` because a long narration is where drift is most audible: across several hundred paragraphs, a narrator whose timbre wanders is worse than one that is a little plain. If your title wants a performed read — fiction, dialogue-heavy work, anything theatrical — ask for it:
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d "{\"voice_id\": 389, \"chapter_ids\": [\"$CHAPTER_ID\"], \"include_completed\": true,
\"narration_settings\": {\"delivery_mode\": \"creative\"}}"
```
`delivery_mode` and [Auto Direction](#auto-direction) work on different things and compose well: delivery mode sets how much range the voice has at all, direction says what to do with it line by line. A directed book on `creative` is the most expressive result available.
Also accepted in `narration_settings`: `speed` (0.5–2.0) and `language` (a bare subtag such as `"it"`, overriding the project's language for this run).
**Response** (`BatchNarrationResponse`):
The narration job.
Paragraphs queued for this run.
Paragraphs excluded by the `include_*` rules above (already completed, locked, or skipped), plus passages waiting for review.
Of `skipped_paragraphs`, how many are waiting for [review](#narration-review). A batch never narrates these again, whatever the `include_*` flags or `paragraph_ids` say — approve, reject or edit them.
Human-readable estimate.
```bash theme={null}
# whole project
curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"voice_id":387}'
# => {"job_id": 1042, "total_paragraphs": 318, "skipped_paragraphs": 0,
# "estimated_completion_time": "Queued"}
# just two chapters
curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"voice_id":387,"chapter_ids":["",""]}'
```
```python theme={null}
# whole project
requests.post(f"{AP}/audiobook/projects/{pid}/narration/batch",
headers=H, json={"voice_id": 387})
# or a specific chapter
requests.post(f"{AP}/audiobook/projects/{pid}/narration/batch",
headers=H, json={"voice_id": 387, "chapter_ids": [chapter_id]})
```
Common non-200s: **`402`** insufficient balance (nothing is queued or charged), **`409`** another narration/parse/mix/export is already live for this project, **`503`** the job could not be queued (nothing charged — retry).
### Track progress
`GET /audiobook/projects/{project_id}/narration/progress` returns a `NarrationProgress`:
Total chapters.
Finished narrations.
Failed narrations.
Currently rendering.
Per-chapter status detail. Each entry carries `needs_review` — the number of passages in that chapter waiting for review — and its `status` is `needs_review` when any are (and nothing is narrating or failed).
Passages waiting for [review](#narration-review), across the book.
Narration is complete when `completed_chapters + failed_chapters >= total_chapters`. (Or skip polling and wait for the [completion webhook](#completion-webhooks).)
### Paragraph-level & regeneration
For a single line, use [`POST .../paragraphs/{paragraph_id}/narrate`](#narrate-a-single-paragraph). To re-render a whole chapter after an edit, `POST .../projects/{project_id}/narration/batch` with `{"chapter_ids": [""], "include_completed": true}` (add `voice_id` to swap the voice at the same time) — `include_completed` is what makes it a *re*-render, since the chapter's paragraphs are already complete and would otherwise all be skipped. To re-queue only the paragraphs of a chapter whose status is `failed`, `POST .../projects/{project_id}/chapters/{chapter_id}/retry-failed-paragraphs`.
### Cancel a narration run
`POST /audiobook/projects/{project_id}/narration/cancel` stops a narration run that is still in progress. No request body is required.
Cancelling is **resume-friendly, not destructive**:
* Paragraphs that already finished keep their audio — nothing you have paid for is discarded.
* Paragraphs that had not produced audio yet are returned to a re-narratable state, and the charges held for them are released.
* To carry on later, call `POST .../narration/batch` again: it skips the paragraphs that are already done and picks up only what is left.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/audiobook/projects/{project_id}/narration/cancel" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
If no narration is running, the endpoint returns `404` with `detail.error_code = "no_narration_running"`. That is also what a **repeat** cancel returns — so a client that retries after a network timeout can treat this as confirmation that the earlier cancel succeeded, not as an error.
If you have a webhook endpoint registered, cancelling emits a [`job.cancelled`](#completion-webhooks) event, so an integration waiting on a narration job learns it has stopped instead of polling until it times out.
Existing webhook endpoints only receive the events they were registered with. If yours was created before `job.cancelled` existed, re-register it (or add the event) to receive cancellations.
### Regenerate a completed chapter
By default, `POST .../narration/batch` **skips** paragraphs that already have completed narration — that's the `"No eligible paragraphs to narrate"` response. To re-narrate them, pass `"include_completed": true`:
```json theme={null}
{
"voice_id": 389,
"chapter_ids": [""],
"include_completed": true
}
```
That body — one `chapter_ids` entry plus `include_completed` — *is* the "re-render one chapter" call. There is no separate endpoint for it; the old `.../chapters/{chapter_id}/regenerate` route is deprecated and returns `410 Gone`.
## Narration review
For automated pipelines, set `narration_review_policy` to `auto_accept` on the project; passages are then never held and your chapters always complete.
After each passage is narrated, our narration check confirms the audio matches the text. Short or highly expressive lines (laughter, interjections, a phrase in another language) occasionally can't be confirmed, so instead of failing the book, AudioPod handles them like this:
1. **Retry once.** A passage whose audio doesn't match is narrated again — at most 2 automatic renders per passage.
2. **Hold for review.** If it still doesn't match, the passage is set to `needs_review` and its best render is kept as a **candidate**. It is **never re-narrated automatically** — no batch, retry or recovery will render it again; only reject with `regenerate`, a single-paragraph narrate, or a text edit renders it again.
3. **Resolve it.** Held passages **block chapter assembly and export** until you decide:
* **Approve** — the candidate becomes the passage's audio as a new take. Approving is **free** (the candidate's narration was already refunded). On a per-finished-hour book it still counts toward your narration hours.
* **Approve all** — the same, for every waiting passage in the book or one chapter.
* **Reject** — the candidate is discarded and the passage returns to `pending`. Pass `regenerate: true` to narrate it again right away, billed like any narration.
* **Edit the text** (`PUT .../paragraphs/{paragraph_id}`) — the candidate no longer applies and is retired; the passage returns to pending for a normal narration. Restoring an earlier take also resolves it.
**Replacement candidates.** When a passage that already had approved audio is narrated again and the new render can't be confirmed, the passage keeps its existing audio (`completed`) and the new render waits as a replacement candidate (`replacement: true`). It appears in the review list like any held passage, and counts toward `needs_review_paragraphs` and the per-chapter `needs_review` in progress. Approve to swap it in; reject to keep the audio you had.
### Review policy — `narration_review_policy`
| Value | Behaviour |
| - | - |
| `manual` (default) | Unconfirmed passages are held for your review; the book waits on them. |
| `auto_accept` | The best render is published and flagged (`auto_accepted: true`), so the book never blocks. You can still list flagged passages with `status=all_flagged`. |
Set it on project create, on `PUT /audiobook/projects/{project_id}`, or in the `narration/batch` body (saved on the project). **For fully automated pipelines with no human in the loop, use `auto_accept`.**
```bash theme={null}
curl -X PUT "$AP/audiobook/projects/$PID" -H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" -d '{"narration_review_policy":"auto_accept"}'
```
### Knowing when review is needed
* `GET .../narration/progress` returns `needs_review_paragraphs` (book total) and a `needs_review` count per chapter in `chapter_status`. Both include held replacement candidates (whose passage stays `completed`).
* `POST .../narration/batch` returns `skipped_needs_review` — held passages are skipped whatever the `include_*` flags or `paragraph_ids` say. When the only passages left are held for review, the batch returns `400` telling you how many wait for review (approve, reject or edit them).
* `POST .../chapters/{chapter_id}/retry-failed-paragraphs` returns `needs_review` — held passages are not failures and are never retried there.
* A [`narration.review_required`](/account/webhooks#narration-review-required) webhook fires once per narration job that finished with passages waiting.
* `POST .../export` returns `422` with `detail.code = "narration_review_pending"` and the `count` of waiting passages.
### List passages for review
`GET /audiobook/projects/{project_id}/review`
`needs_review`: passages waiting for a decision. `all_flagged`: also passages accepted automatically or already approved.
```bash theme={null}
curl "$AP/audiobook/projects/$PID/review?status=needs_review" -H "X-API-Key: $AUDIOPOD_API_KEY"
```
```json theme={null}
{
"items": [
{
"paragraph_id": "7d0c…",
"chapter_id": "a41e…",
"chapter_number": 3,
"position": 12,
"status": "needs_review",
"expected_text": "Ha! Not on your life.",
"heard_text": "Ha ha. Not on your life.",
"coverage": 0.71,
"candidate_audio_url": "https://media.audiopod.ai/…mp3?…",
"duration_seconds": 2.4,
"flagged_at": "2026-10-08T09:14:02Z",
"attempts": 2,
"auto_accepted": false,
"approved_at": null,
"replacement": false
}
],
"total": 1
}
```
The passage text.
What our narration check heard in the candidate.
How much of the text the check matched (0–1).
Short-lived (1 hour) link to the candidate audio; re-list to refresh it.
Renders tried.
Published automatically under `auto_accept`.
Set once approved; `null` while waiting.
The candidate would replace audio the passage already has.
### Approve one passage
`POST /audiobook/projects/{project_id}/paragraphs/{paragraph_id}/review/approve` — no body. Returns the updated paragraph (status `completed`). The chapter is rebuilt once nothing else in it is outstanding. Approving an already-approved passage returns it unchanged.
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID/review/approve" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "id": "7d0c…", "status": "completed", ... }
```
### Approve all
`POST /audiobook/projects/{project_id}/review/approve-all`
Optional — only approve passages in this chapter. Omit the body to approve the whole book.
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/review/approve-all" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d "{\"chapter_id\": \"$CHAPTER_ID\"}"
# => { "approved": 3, "refused": 1, "remaining": 1,
# "refused_reasons": { "9b2f…": "narration_allowance_exhausted" } }
```
A passage that can't be approved is left waiting and reported; the call still returns `200` with a partial result.
Passages approved by this call.
Passages that could not be approved.
Passages still waiting for review in the selection.
`{paragraph_id: reason_code}` for each refusal: `narration_allowance_exhausted` (a book bought per finished hour has no hours left for this audio), `review_stale` (the passage has newer narration) or `review_candidate_unavailable`.
### Reject one passage
`POST /audiobook/projects/{project_id}/paragraphs/{paragraph_id}/review/reject`
Narrate the passage again right away (billed like any narration).
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID/review/reject" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"regenerate": true}'
# => { "paragraph_id": "7d0c…", "status": "queued",
# "regenerate": { "task_id": "…", "paragraph_id": "7d0c…", "status": "queued", "billing": { … } } }
```
Without `regenerate`, the response is `{"paragraph_id": "…", "status": "pending", "regenerate": null}` (or `"completed"` for a replacement candidate — the passage keeps its existing audio).
### Review errors
| Status | When |
| - | - |
| `409` | Approve and reject return `{"detail": {"code": "…", "message": "…"}}`. `review_not_pending`: nothing waits for review on this passage. |
| `409` | Approve: `review_stale` (the passage has newer narration than the candidate), `review_candidate_unavailable`, or, for a book bought per finished hour, `narration_allowance_exhausted` / `narration_allowance_changed` (approved audio counts toward the book's hours). |
| `409` | Approve or approve-all while an export is running for the project. |
| `404` | The paragraph is not in this project. |
| `422` | `POST .../export` while passages wait: `{"detail": {"code": "narration_review_pending", "message": "…", "count": 3}}`. |
## 6. Production extras (optional)
Add intro/outro music, narrated opening/closing credits, or a cover before export:
* `GET·PUT /audiobook/projects/{project_id}/production-settings` — `ProductionMusicSettings` (`intro`, `outro`, `credits`, `cover`, `silence`).
**This is where the spoken title/author announcement is configured.** Set `credits.narrator_name` (plus `include_opening` / `include_closing`, both default `true`) and the export produces narrated `Chapter_00_*_Credits.mp3` (title, subtitle, author, narrator) and `Chapter_99_*_Credits.mp3` as separate files in the ACX package — the opening announcement Audible/ACX expect. **No credits files are produced unless this is set.**
```json theme={null}
PUT /audiobook/projects/{project_id}/production-settings
{"credits": {"narrator_name": "Jane Doe", "include_opening": true, "include_closing": true}}
```
Previously named `/music-settings`. That path still works and will keep working, but it only ever described 2 of the 4 fields — use `production-settings`.
* `POST /audiobook/projects/{project_id}/media/upload` — upload a cover image or custom music track to reference from the settings above.
### Chapter silence (room tone)
Every exported chapter opens and closes with digital silence — this is why an
exported MP3 has leading and trailing silence, and it is a retail requirement,
not padding you should trim. The same values apply to the per-chapter download
and to each chapter inside the ACX package, so an audition always matches what
ships.
Defaults are **750 ms head / 3000 ms tail** (mid-window for ACX). Override per
project with `silence`:
```json theme={null}
PUT /audiobook/projects/{project_id}/production-settings
{"silence": {"head_ms": 2000, "tail_ms": 2000}}
```
Leading silence, in milliseconds. Accepted range **0–5000**.
Trailing silence, in milliseconds. Accepted range **0–10000**.
Read-only. `true` when `head_ms` is 500–1000 and `tail_ms` is 1000–5000 — the ACX opening/closing silence requirements.
`silence` is always returned, populated with the defaults when you have never
set it, so you can read the effective values without knowing them.
Values outside the **ACX** range are **accepted, not rejected** — plenty of
deliveries are not Audible, and a house standard of 2 s/2 s is a legitimate
choice. Each out-of-range field adds an entry to the response's `warnings`
array naming the field and the ACX range, and `acx_compliant` flips to `false`:
```json theme={null}
{ "silence": { "head_ms": 2000, "tail_ms": 2000, "acx_compliant": false },
"warnings": ["head_ms 2000 is outside the ACX range 500–1000 ms; files may be rejected by ACX QA."] }
```
Values outside the **accepted range** (a negative `head_ms`, a `tail_ms` above
10000\) are a `422`. Send `"silence": null` to reset both sides to the defaults;
omit the field entirely to leave them unchanged.
If you are delivering to ACX/Audible, keep `acx_compliant` at `true`. Out-of-range silence is one of the most common causes of a retail QA rejection, and the rejection arrives after upload — not at export time.
## 7. Export the ACX package
`POST /audiobook/projects/{project_id}/export` runs an automatic ACX compliance check, then assembles the package.
Optional — export only these chapters.
Optional — override export settings.
**Response** (`ACXExportResponse`):
Export job ID.
An `ACXComplianceCheck` (see below).
Any additional TTS cost incurred by the export (0 when audio already exists).
**`ACXComplianceCheck`:**
Whether the book passes ACX requirements.
Blocking problems (export is refused when non-empty).
Non-blocking advisories.
Container/bitrate compliant.
RMS/peak within ACX bounds.
Head/tail room present.
File names follow the ACX convention.
If a book is non-compliant, the call returns the specific issues instead of producing an invalid package:
```json theme={null}
{ "detail": { "message": "Project is not ACX-compliant. Fix these issues first.",
"issues": ["Chapter 3: 28s < ACX minimum 30s"], "warnings": [] } }
```
Poll `GET .../export/{job_id}/status` until `COMPLETED`, then fetch the download:
`GET /audiobook/projects/{project_id}/export/{job_id}/download` returns a JSON manifest with a **7-day presigned `download_url`** to the ZIP:
```json theme={null}
{ "download_url": "https://media.audiopod.ai/...acx_package_xxx.zip?...",
"file_size_bytes": 2079760, "file_format": "zip", "chapter_count": 2 }
```
### The ACX package
| File | Contents |
| - | - |
| `Chapter_NN.mp3` | One per chapter — MP3, **192 kbps CBR, 44.1 kHz** |
| `00_retail_sample.mp3` | Retail / audition sample |
| `Chapter_00/99_*_Credits.mp3` | Optional narrated opening/closing credits (when configured) |
| `metadata.json` | Title, author, narrator, per-chapter durations, format spec |
| `manifest.csv` | Flat file list |
| `provenance.json` | Generation provenance |
## Completion webhooks
Rather than poll, register an endpoint and receive a signed event the moment a job finishes — the recommended pattern for backlog-scale automation.
| Operation | Method | Endpoint |
| - | - | - |
| Register endpoint | POST | `/webhooks/endpoints` |
| List endpoints | GET | `/webhooks/endpoints` |
| Send a test event | POST | `/webhooks/endpoints/{endpoint_id}/test` |
| Delivery log | GET | `/webhooks/endpoints/{endpoint_id}/deliveries` |
| Redeliver | POST | `/webhooks/deliveries/{delivery_id}/redeliver` |
```bash theme={null}
curl -X POST "$AP/webhooks/endpoints" -H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-app.com/hooks/audiopod","events":["job.completed","job.failed","job.cancelled"]}'
# => { "id": "...", "url": "...", "events": [...], "secret": "whsec_..." } # secret shown ONCE
```
Every delivery carries these headers:
| Header | Meaning |
| - | - |
| `X-AudioPod-Signature` | `sha256=` HMAC of the payload (see below) |
| `X-AudioPod-Timestamp` | Unix seconds; part of the signed material (replay protection) |
| `X-AudioPod-Event` | `job.completed`, `job.failed`, `job.cancelled`, or `narration.review_required` |
| `X-AudioPod-Event-Id` | Stable UUID per event — **dedupe on this** (delivery is at-least-once) |
**Verify the signature** — HMAC-SHA256 over `"."` with your endpoint secret as the key:
```python theme={null}
import hmac, hashlib
def verify(secret: str, raw_body: bytes, signature_header: str, timestamp_header: str) -> bool:
expected = hmac.new(
secret.encode(),
timestamp_header.encode() + b"." + raw_body, # exact raw bytes received
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature_header)
```
Delivery is retried with exponential backoff and parked in a dead-letter queue after repeated failures; inspect and redeliver from the delivery log. Endpoints are SSRF-guarded (the resolved IP is re-checked at delivery time; https-only; private/internal ranges blocked).
## Status values
`draft` → `parsing` → `ready_for_narration` → `narrating` → `ready_for_production` → `mixing` → `ready_for_export` → `exporting` → `completed`. `failed` if a stage cannot complete (with the reason in `error_message`).
Note that the state *after a successful parse* is `ready_for_narration`, not `parsing` or `completed` — pair it with `parsing_completed_at` when waiting on a parse.
`pending` → `queued` → `narrating` → `completed`, or `failed` with an `error_message`. A paragraph can also be `needs_review` (held for [narration review](#narration-review)).
Jobs are prepaid: credits are reserved at job start, settled on success, and **released on failure** — a failed narration or export never leaves a stranded charge. Narration and export retry automatically; a `job.failed` webhook fires if a job ultimately can't complete, so an automated integrator can react deterministically.
## Error Handling
The export found blocking issues (e.g. a chapter under the 30-second minimum). The response lists each issue under `detail.issues`. Fix the flagged chapters (lengthen, merge, or `skip_narration`) and re-export.
Chapters under \~50 characters can't be narrated (often a stray title-page fragment from PDF parsing). Mark it `skip_narration: true` via `PUT .../chapters/{id}`, or merge it into an adjacent chapter, then continue.
Some passages are waiting for [narration review](#narration-review). `detail.count` says how many. List them with `GET .../review`, then approve, reject or edit them and export again.
Narration/export draws from your prepaid balance. Top up the [API Wallet](/account/api-wallet) and retry; reserved credits for a failed job are released automatically.
Parsing, narration, mixing, and export are mutually exclusive per project — each would rewrite structure the other is reading. Poll `GET .../narration/progress` (or the project's `status`) and retry once the live job finishes. A `409` on a single paragraph means it is `locked`; unlock it via `PUT .../paragraphs/{paragraph_id}` first.
`POST .../narration/start` is deprecated and removed. Send `POST .../narration/batch` instead — with no `chapter_ids` and no `paragraph_ids` it narrates the whole project.
Deprecated and removed. Send `POST .../narration/batch` with `{"chapter_ids": [""], "include_completed": true}`. Without `include_completed` the request succeeds but narrates nothing — every paragraph in an already-rendered chapter is filtered out as complete, and you get `400 No eligible paragraphs to narrate`.
You are probably sending `skip`/`limit`. Unknown query parameters are ignored rather than rejected, so the request still returns `200` — with the first page, every time. The project and chapter list endpoints page with `page` and `per_page`; loop on `has_next`.
Send a valid `X-API-Key` header. Keys are created in the [dashboard](https://www.audiopod.ai/dashboard/account/api-keys) and can expire — check the key's status if previously working calls start returning 401.
## Pricing
Usage-based on a prepaid wallet — you pay per 1,000 characters of text narrated, with no per-seat fee or monthly minimum.
| Service | Rate |
| - | - |
| Narration | \$0.09 per 1,000 characters |
| Voice cloning | \$0.05 per 1,000 characters |
| Transcription (if used) | \$0.01 / min |
A 10-hour audiobook (\~560,000 characters) costs roughly \*\*$50**, versus $2,000–\$4,000 for a human ACX narrator. For committed monthly volume across many titles, [contact us](mailto:sales@audiopod.ai) about partner pricing below list. See [API Wallet](/account/api-wallet) for top-ups and balance.
## Next Steps
Create custom narrator voices from a short sample, then narrate with them.
Top up, check your balance, and see per-minute pricing.
API keys, scopes, and how to authenticate every request.
The shortest path from zero to a generated audiobook.
# Characters and voices (cast)
Source: https://docs.audiopod.ai/api-reference/audiobook-cast
Give each character in an audiobook its own voice through the API: find who speaks each line, build the book's cast, merge alternate names, choose a voice per character, then narrate.
## Overview
By default every line of a book is read by one narrator: the project's `selected_voice_id`. **Character voices** give the characters their own voices. Every paragraph carries a `character_name` (empty means the narrator). The book keeps a **cast**: one voice per character. When character voices are on, each character's lines are narrated with that character's cast voice.
```
add manuscript (detect_speakers / multi_voice) → review cast → merge aliases → choose voices → narrate
```
Every endpoint on this page accepts an API key (`X-API-Key`) as well as a session token, and works only on projects you own. All paths are relative to `https://api.audiopod.ai/api/v1`.
### Turning character voices on
Character voices are **off by default**. While they are off, the book narrates exactly as it always has: one narrator, plus any voice you set on a chapter or a single paragraph. Turn them on with `multi_voice: true` in any of these places:
| Where | Field |
| - | - |
| When adding the manuscript | `multi_voice` on `POST .../manuscript/parse`, `.../manuscript/paste` or `.../manuscript/generate`, or the `multi_voice` form field on `POST .../manuscript/upload` |
| On an existing book | `PUT /audiobook/projects/{project_id}` with `{"multi_voice": true}` |
| When narrating | `multi_voice` on `POST .../narration/batch` (saved on the book before the run reads its voices) |
| By choosing a character's voice | [`POST .../cast/assign`](#3-choose-a-voice-for-a-character) for a character, or [auto-cast](#auto-cast-design-a-voice-per-character), turns it on |
Turning character voices on by any of these routes (or sending `multi_voice: true` again) does two things at no cost:
* every speaker in the book gets a row in the cast;
* **each character without a cast voice gets a library voice automatically, when a suitable one is available.** When `cast/assign` or auto-cast is what turns them on, the character you chose keeps its voice and every other character without a cast voice gets a library voice on the same terms. The pick prefers voices whose own language is the book's language, then voices that support it, then, when the character has a gender hint, a voice of that gender. It never takes the narrator's voice or a voice another character already has. If no suitable library voice is left for the book's language, the character stays without a cast voice and its lines read in the narrator voice; give it one with `cast/assign`. The pick is stable, so the same book is cast the same way every time. Change any pick with `cast/assign`.
Send `multi_voice: false` to turn them off. Lines then follow the [voice order](#voice-order) for character voices off: a line voiced only through the cast goes back to the chapter's voice or the narrator, while a voice saved on the line stays with it (one you set, a batch `voice_id` it was narrated with, or the narrator voice narration saved on it). Locked lines keep the voice they had. The cast is kept for when you turn character voices back on.
The project response carries `multi_voice` (`true` or `false`).
### Finding the speakers
Character voices need to know who speaks each line. Speaker detection fills `character_name`, free. There are two passes: an instant pass over the text, described here, and [our speaker detection pass](#speaker-detection-pass-with-character-voices-on), which reads each line in context and runs by itself when character voices are on.
| Field (on `parse`, `paste`, `generate`) | Default |
| - | - |
| `detect_speakers` | **Off for API-key requests**, on in the web studio. `multi_voice: true` turns it on. |
Detection reads a chapter in order and attributes a paragraph to a named character when it is clearly that character's speech:
* dialogue in `«…»`, `“…”`, `"…"`, or opened with an em dash (`— Buon giorno, — disse Geppetto.`);
* a reporting verb next to a name, in English or Italian (`said Eleanor`, `Eleanor said`, `disse Geppetto`, `gridò mastr'Antonio`), titles included (`maestro Ciliegia`, `Captain Reyes`);
* a narration paragraph ending in `:` that names who speaks next;
* the turn-taking of a two-person exchange: once both speakers are known, untagged lines that follow alternate between them. Anything ambiguous (a third speaker, "he said", a line addressing the person it would be given to) stops the alternation.
The instant pass is built for precision, so when in doubt a line stays with the narrator. A paragraph is voiced whole, so it is given to a character only when at least 60% of its words are speech; a long narration with a short quote stays with the narrator. Detection only fills paragraphs that have no `character_name` and are not locked. It never replaces a name you set, and a paragraph whose speaker you cleared stays with the narrator.
You can also run either pass later with [`POST .../cast/detect`](#detect-speakers-on-an-existing-book), or use [screenplay annotation](#split-mixed-paragraphs-screenplay-annotation) to split mixed paragraphs into separate lines.
### Voice order
When a paragraph is narrated, its voice is the first of these that applies.
**Character voices off** (the default):
1. a voice sent for that one paragraph (`POST .../paragraphs/{id}/narrate` with `voice_id`), or a `voice_id` sent on `narration/batch`;
2. the paragraph's own `voice_id`;
3. the chapter's `voice_id`;
4. the project's `selected_voice_id` (the narrator).
**Character voices on** (`multi_voice: true`):
1. a voice sent for that one paragraph (`POST .../paragraphs/{id}/narrate` with a different `voice_id`);
2. a voice **you** set on the paragraph (`PUT .../paragraphs/{id}` with `voice_id`);
3. the character's cast voice;
4. a `voice_id` sent on `narration/batch` (a narrator for that run);
5. the chapter's `voice_id`;
6. the project's `selected_voice_id` (the narrator).
With character voices on, a batch `voice_id` and a chapter `voice_id` **replace the narrator only**: character lines keep their cast voice, and a line you gave its own voice keeps it. A voice you set for a single line is yours and outranks the cast: a `PUT` with `voice_id`, or narrating one paragraph with a different `voice_id`. Send `{"voice_id": null}` in a `PUT` to release it.
Narration saves the voice it used on each line it narrates, except a line voiced through the cast. A batch `voice_id` is a narrator override, and it is saved this way on the narrator lines it voices. With character voices off, a batch `voice_id` voices every selected line and replaces the voice saved on it, including one you set with a `PUT`. While character voices are off, a saved voice comes before the chapter and narrator voices, so changing the narrator does not reach lines that are already narrated. With character voices on, a saved voice you did not set is ignored, so a narrator or cast change reaches every line that is narrated again; it applies again if you turn character voices off.
| Step | Method | Endpoint | Cost |
| - | - | - | - |
| Turn character voices on or off | PUT | `/audiobook/projects/{project_id}` (`multi_voice`) | Free |
| Read the cast | GET | `/audiobook/projects/{project_id}/cast` | Free |
| Detect speakers | POST | `/audiobook/projects/{project_id}/cast/detect` | Free |
| Split paragraphs into speaker lines | POST | `/audiobook/projects/{project_id}/screenplay/annotate` | Free |
| Annotation status | GET | `/audiobook/projects/{project_id}/screenplay/annotate/{job_id}/status` | Free |
| Merge an alternate name | POST | `/audiobook/projects/{project_id}/cast/alias` | Free |
| Choose a voice for a character | POST | `/audiobook/projects/{project_id}/cast/assign` | Free |
| Tag or voice one paragraph | PUT | `/audiobook/projects/{project_id}/paragraphs/{paragraph_id}` | Free |
| Design a new voice per character | POST | `/audiobook/projects/{project_id}/cast/auto` | **Prepaid**, see [Auto-cast](#auto-cast-design-a-voice-per-character) |
| Auto-cast status | GET | `/audiobook/projects/{project_id}/cast/auto/{job_id}/status` | Free |
Casting itself costs nothing, except auto-cast, which designs new voices. You pay when you [narrate](/api-reference/audiobook#5-narrate), at the normal narration rate, whichever voice reads the line.
**Cast before you narrate.** A new voice only takes effect on a line's next narration, so a voice change on a narrated book means narrating those lines again, at the normal rate.
## Quick start
```bash theme={null}
export AUDIOPOD_API_KEY="ap_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export AP="https://api.audiopod.ai/api/v1"
# PID = a project you created (with "language" and "selected_voice_id", the narrator)
# 1. Add the manuscript with character voices on: speakers are detected and each
# character gets a free library voice while suitable ones are left (check
# voice_id in step 2; null = narrator voice until you cast/assign one). Our speaker detection pass then runs in
# the background, and narration started meanwhile waits for it
curl -s -X POST "$AP/audiobook/projects/$PID/manuscript/paste" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"text": "Capitolo 1\n\n...", "multi_voice": true, "auto_direction": true}'
# 2. Review who speaks, how often, and with which voice
curl -s "$AP/audiobook/projects/$PID/cast" -H "X-API-Key: $AUDIOPOD_API_KEY" \
| jq '.members[] | {character_name, is_narrator, paragraph_count, voice_id}'
# 3. Merge an alternate name onto the main one
curl -s -X POST "$AP/audiobook/projects/$PID/cast/alias" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"character_name": "mastr'"'"'Antonio", "alias_of": "Maestro Ciliegia"}'
# 4. Change any voice you do not like (ids from GET /audiobook/voices/available)
curl -s -X POST "$AP/audiobook/projects/$PID/cast/assign" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"character_name": "Maestro Ciliegia", "voice_id": 1248}'
# 5. Confirm you hold the audio rights (once per book). Without it narration returns
# 400 RIGHTS_NOT_ATTESTED
curl -s -X POST "$AP/audiobook/projects/$PID/rights-attestation" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "ok": true, "rights_attested_at": "2026-10-12T05:39:51.309434" }
# 6. Narrate the whole book
curl -s -X POST "$AP/audiobook/projects/$PID/narration/batch" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}'
```
For a file, send `"multi_voice": true` with `file_key` and `filename` on `POST .../manuscript/parse`.
## 1. Find who speaks each line
Besides detection when you add the manuscript, two calls fill `character_name`. Neither touches locked paragraphs.
### Speaker detection pass with character voices on
When a book has character voices on, adding a manuscript (`parse`, `paste`, `generate`, or a `parse` after an `upload` that sent `multi_voice`) runs a second, free pass after the text is parsed: our speaker detection reads each dialogue paragraph in context. It:
* gives a paragraph a speaker only when it is confident, and leaves narration and unnamed speakers with the narrator;
* fills only paragraphs that have no `character_name`, are not locked, and whose speaker you did not clear;
* merges alternate names of the same character as [aliases](#4-merge-alternate-names) (an alias you set is never changed);
* records whether each character is male or female when the text shows it. The hint is used only for a character that has no cast voice when the pass finishes: it does not change a library voice already picked (for example when the manuscript was parsed). To change such a voice, use `cast/assign`.
Narration of a passage that has not started waits until this pass finishes, so lines are not narrated before their speaker is known. When you asked for `auto_direction`, direction is written after the pass. The pass covers books up to the same word limit as [annotation](#split-mixed-paragraphs-screenplay-annotation).
### Detect speakers on an existing book
`POST /audiobook/projects/{project_id}/cast/detect`
`heuristic`: the instant pass, answered at once. `llm`: [our speaker detection pass](#speaker-detection-pass-with-character-voices-on), run as a background job; the response carries `job_id`.
Either method is free and only fills paragraphs that have no `character_name` and are not locked, so it never overwrites a name you set. With character voices on, each newly found character gets a library voice when the catalog has a suitable one left (one that suits the book's language and is not the narrator's or another character's); a character left without one reads in the narrator voice until you give it one with `cast/assign`.
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/cast/detect" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "detected_paragraphs": , "members": [ ...same shape as GET /cast... ],
# "method": "heuristic", "job_id": null }
curl -X POST "$AP/audiobook/projects/$PID/cast/detect?method=llm" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "detected_paragraphs": 0, "members": [ ... ], "method": "llm", "job_id": "2107" }
```
Paragraphs given a speaker by this call. Always `0` for `method=llm`; read the count from the job status.
The cast after detection, same shape as [`GET /cast`](#2-review-the-cast). For `method=llm`, the cast as it is when the job starts.
`heuristic` or `llm`.
`method=llm` only: the detection job.
Poll `GET /audiobook/projects/{project_id}/cast/detect/{job_id}/status` until `status` is `COMPLETED` or `FAILED`, or subscribe a webhook endpoint to the `audiobook.cast.detected` event:
```bash theme={null}
curl "$AP/audiobook/projects/$PID/cast/detect/2107/status" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "job_id": "2107", "status": "COMPLETED", "progress": 100.0, "current_step": "completed",
# "error_message": null,
# "result": { "paragraphs_attributed": , "characters": ["Geppetto", ...],
# "aliases_added": , "voices_picked": , "targets": } }
```
`PENDING` · `PROCESSING` · `COMPLETED` · `FAILED`. A failed job keeps the speakers it had already written.
Paragraphs given a speaker.
Speakers written to at least one paragraph.
Names merged onto another name of the same character.
Characters given a free library voice (character voices on only).
Dialogue paragraphs the pass looked at.
`method=llm` returns `409` while another detection job runs for the book, `400` when the book is over the annotation word limit, and `503` when speaker detection is unavailable right now.
### Split mixed paragraphs: screenplay annotation
`POST /audiobook/projects/{project_id}/screenplay/annotate`
Our narration engine reads each paragraph and splits it into lines: narration, and dialogue attributed to the named character who speaks it. A paragraph like *"— Ohi! tu m'hai fatto male! — gridò la vocina."* becomes a dialogue line for the speaker plus a narration line. Each line also gets a short performance `direction`. It works in any language and with any dialogue punctuation: quotation marks, guillemets, or the em dashes common in Italian, French and Spanish fiction.
Annotate only these chapters (UUIDs). Omit, or send `{}`, to annotate the whole book. An empty list returns `400` `empty_selection`.
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/screenplay/annotate" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}'
# => { "job_id": "1914", "total_chapters": 2 }
```
The annotation job. Poll its status.
Chapters in scope.
**Annotation is free**, on every plan.
Poll `GET /audiobook/projects/{project_id}/screenplay/annotate/{job_id}/status` until `status` is `COMPLETED` or `FAILED`:
```bash theme={null}
curl "$AP/audiobook/projects/$PID/screenplay/annotate/1914/status" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "job_id": "1914", "status": "COMPLETED", "progress": 100.0, "current_step": "completed",
# "error_message": null,
# "result": { "chapters_annotated": 2, "paragraphs_split": ,
# "characters_found": ["Geppetto", "Maestro Ciliegia", ...],
# "skipped_paragraphs": } }
```
`PENDING` · `PROCESSING` · `COMPLETED` · `FAILED`.
0–100.
Chapters processed.
Paragraphs that were split into more than one line.
Named speakers found.
Paragraphs sent for annotation that were left unchanged: the engine could not annotate them, the result failed the word-count check below, or the paragraph changed (was edited, locked or queued for narration) while the job ran. Locked paragraphs, paragraphs being narrated or queued, and paragraphs marked `skip_narration` are never sent for annotation, so they are not counted here; they are simply left as they are.
What annotation changes, and what it leaves alone:
* **A word-count check guards the text.** A result is only applied when the lines together have within ±5% of the original paragraph's word count. Otherwise the paragraph is left alone and counted in `skipped_paragraphs`. The check counts words; it does not compare them one by one, so read the changed paragraphs before you narrate.
* **Paragraph ids change where a paragraph is split.** The first line keeps the original paragraph's id and new lines are inserted after it. Re-read the chapter's paragraphs after annotation; do not reuse ids you stored before it.
* **Any paragraph whose text changes loses its audio** and goes back to `pending`: every split paragraph, and any paragraph whose text is rewritten even without a split. Annotate before narrating.
* **`character_name` is rewritten** on every unlocked paragraph in scope, including names you set by hand. Lock a paragraph (`PUT .../paragraphs/{id}` with `{"locked": true}`) to protect it.
* **A `direction` you wrote yourself is kept**; the engine's direction fills only the others. See [Auto Direction](/api-reference/audiobook#3c-auto-direction).
* **New characters get a library voice.** With character voices on, each character annotation finds that has no cast voice gets a library voice (never a designed one) when the catalog has a suitable one left (one that suits the book's language and is not the narrator's or another character's); a character left without one reads in the narrator voice until you give it one with `cast/assign`. With character voices off, no voices are picked.
`409` means an annotation is already running for this project, a paragraph in scope is being narrated, or the book is exporting. `400` means no chapters matched `chapter_ids`, or the scope is too large for one pass; annotate a range of chapters instead.
## 2. Review the cast
`GET /audiobook/projects/{project_id}/cast`
```bash theme={null}
curl "$AP/audiobook/projects/$PID/cast" -H "X-API-Key: $AUDIOPOD_API_KEY"
```
```json theme={null}
{
"members": [
{ "character_name": null, "is_narrator": true, "paragraph_count": 75,
"voice_id": 1186, "sample_text": "Capitolo 1.", "alias_of": null, "aliases": [] },
{ "character_name": "Maestro Ciliegia", "is_narrator": false, "paragraph_count": 2,
"voice_id": 1248, "sample_text": "— Insegno l'abbaco alle formicole.",
"alias_of": null, "aliases": ["mastr'Antonio"] },
{ "character_name": "Geppetto", "is_narrator": false, "paragraph_count": 1,
"voice_id": 917, "sample_text": "— Buon pro vi faccia.", "alias_of": null, "aliases": [] },
{ "character_name": "mastr'Antonio", "is_narrator": false, "paragraph_count": 1,
"voice_id": 1248, "sample_text": "— Rendimi la mia parrucca! — gridò mastr'Antonio.",
"alias_of": "Maestro Ciliegia", "aliases": [] }
],
"total_characters": 3
}
```
One entry per speaker: the narrator bucket first, then characters by line count. Names are grouped case-insensitively. A merged alternate name keeps its own entry, with `alias_of` set, and its lines are not counted in the main speaker's `paragraph_count`.
The speaker's name as tagged on the paragraphs. `null` for the narrator.
`true` for the narrator bucket: every paragraph with no `character_name`.
Lines this speaker has (paragraphs marked `skip_narration` are not counted).
For the narrator, the project's `selected_voice_id`. For a character with character voices on, the character's cast voice (an alias shows its main speaker's) when the cast has one. Otherwise, the voice most of the character's lines carry, or `null`.
One of the speaker's lines, for a quick check.
Set when this name is merged onto another speaker.
Names merged onto this speaker.
Named speakers, not counting the narrator.
## 3. Choose a voice for a character
`POST /audiobook/projects/{project_id}/cast/assign`
Pick voice ids from [`GET /audiobook/voices/available`](/api-reference/audiobook#4-choose-a-voice), including your own cloned voices. For a book that is not in English, filter by the full locale (`?language=it-IT`) to see the voices suited to it.
The character to voice (max 120 characters). Matching is case-insensitive and includes every alias of that name.
Set to `true` (or omit `character_name`) to change the narrator instead: the project's `selected_voice_id`.
The voice to use.
```bash theme={null}
# A character
curl -X POST "$AP/audiobook/projects/$PID/cast/assign" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"character_name": "Maestro Ciliegia", "voice_id": 1248}'
# => { "updated_paragraphs": 2, "narrator_default_updated": false, "already_narrated": 0 }
# The narrator
curl -X POST "$AP/audiobook/projects/$PID/cast/assign" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"is_narrator": true, "voice_id": 1186}'
# => { "updated_paragraphs": 0, "narrator_default_updated": true, "already_narrated": 0 }
```
The character's unlocked lines (aliases included). Each takes the cast voice, except a line you gave its own voice.
`true` when the call changed the project's narrator voice.
How many of those lines already have audio. They keep it until you narrate them again.
How it works:
* **The voice is saved on the book's cast**, not copied onto lines. Lines attributed to the character **later** (by detection, annotation or a `PUT`) take the cast voice too. Choosing a character's voice turns character voices on for the book. When this call is what turns them on, every other character without a cast voice gets a free library voice at the same time, as with `multi_voice: true`.
* **Locked lines keep the voice they have.** So does a line you gave its own voice with `PUT .../paragraphs/{id}`.
* **Narrated lines keep their audio.** The new voice is heard on the line's next narration; see [Re-narrate](#5-narrate-and-re-narrate).
* **The narrator** is the project's `selected_voice_id`. Assigning it does not turn character voices on.
## 4. Merge alternate names
Books call the same person different things: *mastr'Antonio* and *Maestro Ciliegia*, *Mr. Darcy* and *Darcy*. An alias makes them one speaker with one voice.
`POST /audiobook/projects/{project_id}/cast/alias`
The alternate name, as it appears in the cast (1–120 characters).
The main name to merge it onto. Send `null` or omit it to remove the alias again.
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/cast/alias" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"character_name": "Darcy", "alias_of": "Mr. Darcy"}'
# => { "updated_paragraphs": 1, "members": [ ...updated cast: "Darcy" now has "alias_of": "Mr. Darcy",
# and "Mr. Darcy" lists it under "aliases" ... ] }
```
Lines under the alternate name whose stored voice was set to the main speaker's.
The cast after the change, same shape as `GET /cast`.
The paragraphs keep their original `character_name`; the merge is stored on the book. With character voices on, the alternate name's lines take the main speaker's cast voice, and locked lines keep the voice they had. `400` means the alias is not allowed, for example a name aliased onto itself.
### One paragraph at a time
`PUT /audiobook/projects/{project_id}/paragraphs/{paragraph_id}` tags or voices a single line, for a correction the bulk calls do not cover:
```bash theme={null}
curl -X PUT "$AP/audiobook/projects/$PID/paragraphs/$PARA_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"character_name": "Geppetto"}'
```
Speaker for this paragraph (max 120 characters). Send `null` to give the line to the narrator. A speaker you set or clear is yours: detection never changes it.
A voice for this line only. It outranks the cast and sticks to the line. Send `null` to release it, so the line follows the [voice order](#voice-order) again.
With character voices on, tagging a line with a character is enough: it takes that character's cast voice. A locked paragraph refuses every change except unlocking it (`409`). Changing these fields does not delete the line's audio; the new voice is heard on its next narration.
## 5. Narrate and re-narrate
Confirm rights once per book with `POST .../rights-attestation` (narration returns `400 RIGHTS_NOT_ATTESTED` until you do), then narrate with [`POST .../narration/batch`](/api-reference/audiobook#5-narrate):
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" -d '{}'
```
Turn character voices on or off for this run and later ones (saved on the book first). `true` also gives uncast characters a library voice.
A narrator for this run. With character voices on it voices only the narrator's lines; with them off it voices every selected line and replaces the voice saved on it. It is saved on the lines it voices, like any narration voice, but does not count as a voice you set for the line.
If a selected line resolves to no voice at all (no cast voice, no chapter voice, no narrator), the batch returns `400`.
To hear a new voice on lines that are already narrated, narrate just those paragraphs again with `paragraph_ids` and `include_completed`. Collect the ids of the character's lines from the chapters' paragraph lists. Paragraphs keep the name they were tagged with, so after an alias merge match the main name **and** every alias (from `GET /cast`), case-insensitively. A main name with no lines of its own has no member of its own; its aliases point to it through `alias_of`:
```bash theme={null}
# the character's name plus its aliases, lower-cased
NAMES=$(curl -s "$AP/audiobook/projects/$PID/cast" -H "X-API-Key: $AUDIOPOD_API_KEY" \
| jq -c --arg n "Maestro Ciliegia" '($n | ascii_downcase) as $k
| [$k] + [.members[]
| select(((.character_name // "") | ascii_downcase) == $k
or ((.alias_of // "") | ascii_downcase) == $k)
| ([.character_name] + .aliases)[] | ascii_downcase] | unique')
# ids of every line Maestro Ciliegia speaks in one chapter, under any of those names
IDS=$(curl -s "$AP/audiobook/projects/$PID/chapters/$CHAPTER_ID/paragraphs" -H "X-API-Key: $AUDIOPOD_API_KEY" \
| jq -c --argjson names "$NAMES" '[.paragraphs[]
| select((.character_name // "" | ascii_downcase) as $c | $names | index($c) != null) | .id]')
# nothing matched: stop (an empty list is refused anyway)
[ "$IDS" = "[]" ] || [ -z "$IDS" ] && { echo "no matching paragraphs - nothing to re-narrate" >&2; exit 1; }
curl -X POST "$AP/audiobook/projects/$PID/narration/batch" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d "{\"paragraph_ids\": $IDS, \"include_completed\": true}"
```
**To narrate the whole book, omit `paragraph_ids` and `chapter_ids`.** An empty list (`[]`) is refused with `400` and `"code": "empty_selection"`, so a filter that matched nothing never narrates, or bills, the whole book. The same rule applies to `chapter_ids` on annotation and Auto Direction, and to `character_names` on auto-cast.
Re-narration is billed like any narration, so only the lines you send are charged. A single line can also be redone with [`POST .../paragraphs/{paragraph_id}/narrate`](/api-reference/audiobook#narrate-a-single-paragraph), where the first regeneration of a paragraph is free.
### Change voices on a narrated book
* **A new narrator, characters on.** `cast/assign` with `{"is_narrator": true, "voice_id": }`, then narrate again with `include_completed: true`. Every narrator line takes the new voice, except chapters with their own `voice_id` (clear it with `PUT .../chapters/{chapter_id}` and `{"voice_id": null}`) and lines you gave their own voice.
* **A new voice for one character.** `cast/assign` the character, then narrate their lines again as shown above.
* **Characters off.** Narration saved the voice it used on each line. To re-voice the whole book, send the voice in the batch: `{"voice_id": , "include_completed": true}`. It voices every selected line and is saved on each, replacing the voice the line had, including one you set with a `PUT`.
## Auto-cast: design a voice per character
`POST /audiobook/projects/{project_id}/cast/auto`
Instead of library voices, auto-cast **designs a new custom voice for each character** from how the book portrays them, saves it to your voices and to the book's cast, and turns character voices on. It is a paid feature with a prepaid charge.
Characters to cast. Omit to cast the speakers with the most lines who are not cast yet. An empty list returns `400` `empty_selection`.
How many characters to cast, `1`–`12`.
```bash theme={null}
curl -X POST "$AP/audiobook/projects/$PID/cast/auto" \
-H "X-API-Key: $AUDIOPOD_API_KEY" -H "Content-Type: application/json" \
-d '{"character_names": ["Maestro Ciliegia", "Geppetto"]}'
# => { "job_id": "2051", "characters": ["Maestro Ciliegia", "Geppetto"] }
curl "$AP/audiobook/projects/$PID/cast/auto/2051/status" -H "X-API-Key: $AUDIOPOD_API_KEY"
# => { "job_id": "2051", "status": "COMPLETED", "progress": 100.0,
# "result": { "cast": [ { "character_name": "Geppetto", "voice_id": 4410, "display_name": "Geppetto" }, ... ],
# "failed": [] } }
```
* **Prepaid.** 250 credits per character are reserved when you start the job. Each is charged when that character's voice is made, and refunded if it fails. If the balance cannot cover every character, the call returns `402` and nothing is charged.
* **Plan.** Auto-cast needs an active paid subscription plan that includes monthly credits. Without one (the free plan, or a paid add-on that grants no credits) it returns `402` with `"code": "PREMIUM_TIER_REQUIRED"`. Library voices (`cast/assign` and the automatic pick) work on every plan.
* **Who is skipped.** Without `character_names`, a character is already cast when its cast voice is one you chose (`cast/assign`) or one auto-cast designed, or one of its lines has a voice you set with a `PUT`. A free library voice from the automatic pick does not count, and neither does a voice narration saved on a line. So omitting `character_names` designs a voice for each character that only has a library pick, up to `max_characters`, at 250 credits per character designed. Name the characters in `character_names` to choose exactly what you pay for.
* `409`: an auto-cast job is already running for this project. `400`: there are no characters to cast; detect speakers first. `503`: auto-cast is temporarily unavailable, and nothing was charged.
## Errors
| Status | Meaning |
| - | - |
| `400` | Invalid alias, an empty selection list (`empty_selection`), no chapters to annotate, scope too large to annotate in one pass, no characters to auto-cast, or a line with no voice to narrate with. |
| `402` | Auto-cast only: insufficient credits, or the plan does not include it. |
| `404` | The project, or the job id, does not exist or is not yours. |
| `409` | The book is exporting, a line in scope is being narrated, the same kind of job is already running, or the paragraph is locked. |
| `422` | The request body failed validation (for example `character_name` over 120 characters). |
## Next steps
The full pipeline: projects, manuscripts, narration, review and ACX export.
Clone a voice from a short sample and cast it as a character.
# Authentication API
Source: https://docs.audiopod.ai/api-reference/auth
Register, login, and manage API keys programmatically
## Overview
AudioPod AI supports two authentication methods:
* **API Key**: Use `X-API-Key` header (recommended for servers)
* **JWT Token**: Use `Authorization: Bearer {token}` header (for sessions)
***
## Registration
### Step 1: Initiate Registration
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/initiate-registration" \
-H "Content-Type: application/json" \
-d '{
"email": "dev@example.com",
"password": "SecurePass123",
"full_name": "Developer Name"
}'
```
**Response:**
```json theme={null}
{
"message": "Verification code sent to your email. Please check your inbox."
}
```
### Step 2: Verify Email
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/verify-registration" \
-H "Content-Type: application/json" \
-d '{
"email": "dev@example.com",
"verification_code": "123456"
}'
```
**Response:**
```json theme={null}
{
"user": {
"id": "uuid",
"email": "dev@example.com",
"full_name": "Developer Name",
"is_verified": true
},
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 3600
}
```
***
## Login
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=dev@example.com&password=SecurePass123"
```
**Response:** Same as verify-registration.
***
## API Keys
### Create API Key
Use JWT token from login/registration:
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "production-key",
"scopes": ["*"],
"expires_in_days": null
}'
```
**Response:**
```json theme={null}
{
"id": "key-uuid",
"name": "production-key",
"api_key": "ap_xxxxxxxxxxxxxxxx",
"scopes": ["*"],
"created_at": "2025-01-01T00:00:00Z",
"expires_at": null
}
```
The `api_key` is only shown once. Save it immediately!
### List API Keys
```bash theme={null}
curl -X GET "https://api.audiopod.ai/api/v1/auth/api-keys" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
### Revoke API Key
```bash theme={null}
curl -X DELETE "https://api.audiopod.ai/api/v1/auth/api-keys/{key_id}" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
***
## Password Reset
### Request Reset
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/forgot-password" \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com"}'
```
### Confirm Reset
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/reset-password" \
-H "Content-Type: application/json" \
-d '{
"token": "reset_token_from_email",
"new_password": "NewSecurePass123"
}'
```
***
## Token Refresh
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/refresh" \
-H "Content-Type: application/json" \
-d '{"refresh_token": "eyJhbGciOiJIUzI1NiIs..."}'
```
***
## Get Current User
```bash theme={null}
curl -X GET "https://api.audiopod.ai/api/v1/auth/me" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Response:**
```json theme={null}
{
"id": "uuid",
"email": "dev@example.com",
"full_name": "Developer Name",
"is_verified": true,
"created_at": "2025-01-01T00:00:00Z"
}
```
***
## Errors
| Code | Error | Description |
| - | - | - |
| `401` | `invalid_credentials` | Wrong email or password |
| `401` | `token_expired` | Access token expired |
| `400` | `email_already_registered` | Email already in use |
| `400` | `invalid_verification_code` | Wrong or expired code |
| `429` | `rate_limit_exceeded` | Too many requests |
***
## Quick Reference
| Endpoint | Method | Auth | Description |
| - | - | - | - |
| `/auth/initiate-registration` | POST | None | Start registration |
| `/auth/verify-registration` | POST | None | Verify email |
| `/auth/token` | POST | None | Login |
| `/auth/refresh` | POST | None | Refresh token |
| `/auth/forgot-password` | POST | None | Request reset |
| `/auth/reset-password` | POST | None | Confirm reset |
| `/auth/me` | GET | Required | Get user info |
| `/auth/api-keys` | GET | Required | List API keys |
| `/auth/api-keys` | POST | Required | Create API key |
| `/auth/api-keys/{id}` | DELETE | Required | Revoke API key |
# Audio Tools
Source: https://docs.audiopod.ai/api-reference/music/audio-tools
Cover generation, reference audio, audio analysis, stem extraction, and music editing with AudioMusic V2.
## Cover / Style Transfer
### `POST /api/v1/music/cover`
Transform existing audio while maintaining structure but changing style, timbre, and genre.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Transform a pop song into jazz
job = client.music.cover(
src_audio_url="https://example.com/pop-song.mp3",
caption="jazz piano version with upright bass and brushed drums",
audio_cover_strength=0.7,
wait_for_completion=True
)
print(f"Cover URL: {job['output_url']}")
# Lighter transformation (more of the original preserved)
job = client.music.cover(
src_audio_url="https://example.com/song.mp3",
caption="acoustic guitar version",
audio_cover_strength=0.4,
lyrics="[Verse 1]\nNew lyrics for the cover version",
wait_for_completion=True
)
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
const job = await client.music.cover({
srcAudioUrl: 'https://example.com/pop-song.mp3',
caption: 'jazz piano version with upright bass',
audioCoverStrength: 0.7,
});
const completed = await client.music.waitForCompletion(job.id);
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/cover" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"src_audio_url": "https://example.com/pop-song.mp3",
"caption": "jazz piano version with upright bass and brushed drums",
"audio_cover_strength": 0.7
}'
```
**Request Parameters:**
| Parameter | Type | Required | Default | Description |
| - | - | - | - | - |
| `src_audio_url` | string | Yes | — | URL of the source audio to transform |
| `caption` | string | Yes | — | Style description for the cover |
| `lyrics` | string | No | null | New lyrics for the cover version |
| `audio_cover_strength` | float | No | 0.7 | Transformation strength. 0.0 = completely new, 1.0 = closest to original |
| `display_name` | string | No | null | Custom name |
| + all [base parameters](/api-reference/music/overview#complete-parameters-reference) | | | | |
***
## Reference Audio
### `POST /api/v1/music/reference`
Generate music guided by a reference audio's acoustic features — timbre, mixing style, spatial characteristics, and overall atmosphere.
```python theme={null}
job = client.music.reference(
reference_audio_url="https://example.com/reference-track.mp3",
caption="upbeat pop song with similar vocal warmth and production style",
lyrics="[Verse]\nHello world\n[Chorus]\nLa la la",
wait_for_completion=True
)
print(f"Reference-guided URL: {job['output_url']}")
```
```typescript theme={null}
const job = await client.music.reference({
referenceAudioUrl: 'https://example.com/reference-track.mp3',
caption: 'upbeat pop song with similar vocal warmth',
lyrics: '[Verse]\nHello world\n[Chorus]\nLa la la',
});
const completed = await client.music.waitForCompletion(job.id);
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/reference" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reference_audio_url": "https://example.com/reference-track.mp3",
"caption": "upbeat pop song with similar vocal warmth",
"lyrics": "[Verse]\nHello world\n[Chorus]\nLa la la"
}'
```
**Request Parameters:**
| Parameter | Type | Required | Default | Description |
| - | - | - | - | - |
| `reference_audio_url` | string | Yes | — | URL of reference audio for style guidance |
| `caption` | string | Yes | — | Text description of desired music |
| `lyrics` | string | No | null | Song lyrics |
| `instrumental` | bool | No | false | Generate instrumental only |
| `display_name` | string | No | null | Custom name |
| + all [base parameters](/api-reference/music/overview#complete-parameters-reference) | | | | |
***
## Audio Analysis
### `POST /api/v1/music/analyze`
Extract metadata from audio using AudioMusic V2. Returns caption, lyrics, BPM, key, time signature, duration, and language.
```python theme={null}
analysis = client.music.analyze(
audio_url="https://example.com/song.mp3"
)
if analysis.get("success"):
print(f"Caption: {analysis['caption']}")
print(f"BPM: {analysis['bpm']}")
print(f"Key: {analysis['keyscale']}")
print(f"Time Sig: {analysis['timesignature']}")
print(f"Duration: {analysis['duration']}s")
print(f"Language: {analysis['language']}")
if analysis.get('lyrics'):
print(f"Lyrics:\n{analysis['lyrics']}")
```
```typescript theme={null}
const analysis = await client.music.analyze({
audioUrl: 'https://example.com/song.mp3',
});
if (analysis.success) {
console.log(`Caption: ${analysis.caption}`);
console.log(`BPM: ${analysis.bpm}`);
console.log(`Key: ${analysis.keyscale}`);
}
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/analyze" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audio_url": "https://example.com/song.mp3"}'
```
**Request Parameters:**
| Parameter | Type | Required | Default | Description |
| - | - | - | - | - |
| `audio_url` | string | Yes | — | URL of audio to analyze |
| `audio_codes` | string | No | null | Pre-extracted semantic codes |
| `temperature` | float | No | 0.85 | LM temperature for analysis (0.0-2.0) |
**Response:**
```json theme={null}
{
"success": true,
"caption": "upbeat pop song with female vocals, electronic beats, 4/4 time",
"lyrics": "[Verse 1]\nWalking down the street...",
"bpm": 120,
"keyscale": "C Major",
"timesignature": "4/4",
"duration": 195.5,
"language": "en",
"audio_codes": "..."
}
```
**Rate limit:** 10 requests per minute.
***
## Stem Extraction
### `POST /api/v1/music/extract-stem`
Extract a specific instrument or vocal track from audio.
```python theme={null}
# Extract vocals
job = client.music.extract_stem(
src_audio_url="https://example.com/song.mp3",
track_name="vocals",
wait_for_completion=True
)
print(f"Vocals URL: {job['output_url']}")
# Extract drums
job = client.music.extract_stem(
src_audio_url="https://example.com/song.mp3",
track_name="drums",
wait_for_completion=True
)
```
```typescript theme={null}
const job = await client.music.extractStem({
srcAudioUrl: 'https://example.com/song.mp3',
trackName: 'vocals',
});
const completed = await client.music.waitForCompletion(job.id);
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/extract-stem" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"src_audio_url": "https://example.com/song.mp3",
"track_name": "vocals"
}'
```
**Request Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `src_audio_url` | string | Yes | URL of audio to extract stem from |
| `track_name` | string | Yes | Instrument to extract (see table below) |
| `display_name` | string | No | Custom name for the job |
**Available Tracks:**
| Track Name | Description |
| - | - |
| `vocals` | Lead vocals |
| `drums` | Full drum kit |
| `bass` | Bass guitar / bass synth |
| `guitar` | Guitar (acoustic/electric) |
| `keyboard` | Piano / keyboard |
| `strings` | String instruments |
| `synth` | Synthesizers |
| `percussion` | Percussion elements |
| `brass` | Brass instruments |
| `woodwinds` | Woodwind instruments |
| `backing_vocals` | Background vocals |
| `fx` | Sound effects |
***
## Retake (Variations)
### `POST /api/v1/music/retake`
Generate variations of an existing music track by re-running with different seeds.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/retake" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"original_job_id": 123,
"retake_variance": 0.7,
"display_name": "Jazz Variation"
}'
```
**Request Parameters:**
| Parameter | Type | Required | Default | Description |
| - | - | - | - | - |
| `original_job_id` | int | Yes | — | ID of the completed original job |
| `retake_variance` | float | No | 0.7 | Variation strength (0.0-1.0) |
| `retake_seeds` | int\[] | No | null | Specific seeds for reproducibility |
| `display_name` | string | No | null | Custom name |
**Rate limit:** 10 requests per minute.
***
## Repaint (Section Regeneration)
### `POST /api/v1/music/repaint`
Regenerate a specific time segment of audio while keeping the rest intact. Useful for fixing problematic sections or changing lyrics in a segment.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/repaint" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_job_id": 123,
"repainting_start": 30.0,
"repainting_end": 60.0,
"caption": "energetic chorus with powerful vocals",
"lyrics": "[Chorus]\nNew chorus lyrics here"
}'
```
**Request Parameters:**
| Parameter | Type | Required | Default | Description |
| - | - | - | - | - |
| `source_job_id` | int | No | null | ID of source job (alternative to `src_audio_url`) |
| `src_audio_url` | string | No | null | URL of source audio (alternative to `source_job_id`) |
| `repainting_start` | float | Yes | — | Start time in seconds |
| `repainting_end` | float | Yes | — | End time in seconds (-1 for end of file) |
| `caption` | string | Yes | — | Style description for the repainted section |
| `lyrics` | string | No | null | Lyrics for the repainted section |
| `display_name` | string | No | null | Custom name |
**Note:** Either `source_job_id` or `src_audio_url` must be provided.
**Rate limit:** 3 requests per minute.
***
## Extend
### `POST /api/v1/music/extend`
Extend an existing music track by adding content at the beginning or end.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/extend" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_job_id": 123,
"right_extend_length": 30.0,
"prompt": "continue with the same energy and add a dramatic outro",
"lyrics": "[Outro]\nThe music fades away"
}'
```
**Request Parameters:**
| Parameter | Type | Required | Default | Description |
| - | - | - | - | - |
| `source_job_id` | int | Yes | — | ID of the completed source job |
| `left_extend_length` | float | No | 0.0 | Seconds to add at the beginning (0.0-60.0) |
| `right_extend_length` | float | No | 30.0 | Seconds to add at the end (0.0-60.0) |
| `prompt` | string | Yes | — | Style description for the extension |
| `lyrics` | string | No | null | Lyrics for the extension |
| `display_name` | string | No | null | Custom name |
**Rate limit:** 3 requests per minute.
***
## Edit
### `POST /api/v1/music/edit`
Edit existing audio with new prompts and lyrics while preserving the overall structure.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/edit" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_job_id": 123,
"edit_target_prompt": "make the guitars more prominent and add reverb",
"display_name": "Guitar Edit"
}'
```
**Request Parameters:**
| Parameter | Type | Required | Default | Description |
| - | - | - | - | - |
| `source_job_id` | int | Yes | — | ID of the completed source job |
| `edit_target_prompt` | string | No | null | New style/prompt for the edit |
| `edit_target_lyrics` | string | No | null | New lyrics |
| `edit_type` | string | No | null | Edit type identifier |
| `edit_n_min` | float | No | null | Edit range minimum |
| `edit_n_max` | float | No | null | Edit range maximum |
| `display_name` | string | No | null | Custom name |
**Rate limit:** 3 requests per minute.
***
## LRC Timestamp Generation
### `POST /api/v1/music/{job_id}/lrc`
Generate synchronized LRC (lyric) timestamps for a completed music job.
LRC generation requires internal tensors from the music generation process. For best results, enable `generate_lrc: true` when creating the music generation request. This endpoint is for retroactive LRC generation which has limitations.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/123/lrc" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json"
```
**Response:**
```json theme={null}
{
"success": true,
"job_id": 123,
"lrc_content": "[00:00.00] Walking down the street\n[00:05.20] On a sunny day...",
"lrc_url": "https://media.audiopod.ai/lrc/123.lrc"
}
```
**Rate limit:** 20 requests per minute.
# Choosing how to make music
Source: https://docs.audiopod.ai/api-reference/music/choosing
Which AudioMusic endpoint to call: Song mode for finished tracks, Score-first for songs you want to shape before they are recorded.
AudioMusic works in two modes, and almost every request belongs to the first.
* **Song** — the default. Describe a song (or bring lyrics) and get a finished,
mixed track. It honors a requested length, can write the lyrics for you, and
returns synced lyrics. It is also the mode for everything that starts from
audio you already have.
* **Score-first** — for when you want to see and shape the composition itself
(melody, chords, sections) before it is recorded. You get an editable score
first, change it in plain words as often as you like, and record it when it
is right.
There is no single "make music" endpoint: pick the route from the table below.
## Routing table
| You want | Mode | Endpoint |
| - | - | - |
| A song from a description ("a sad indie song about rain") — AudioPod writes the lyrics | Song | [`POST /api/v1/music/simple`](/api-reference/music/overview) |
| A song with your own lyrics | Song | [`POST /api/v1/music/text2music`](/api-reference/music/generation), `lyric2vocals` or `text2rap` |
| An instrumental or beat | Song | [`POST /api/v1/music/prompt2instrumental`](/api-reference/music/generation) |
| A sound effect or sample | Song | [`POST /api/v1/music/text2samples`](/api-reference/music/generation) |
| An exact length, or synced lyrics (LRC) | Song | any Song generation route |
| A new version of existing audio, or "like this reference" | Song | [`/cover`, `/reference`](/api-reference/music/audio-tools) |
| Extend, retake or repaint a track | Song | [extend / retake / repaint](/api-reference/music/audio-tools) |
| "Write it so I can change it" | Score-first | [`POST /api/v1/music/v3/plan`](/api-reference/music/score-first#write-a-score) |
| Changes to a score, in plain words | Score-first | [`POST /api/v1/music/v3/revise`](/api-reference/music/score-first#revise-a-score-in-plain-words) |
| Record a score | Score-first | [`POST /api/v1/music/v3/edit`](/api-reference/music/score-first#record-a-score) |
| The same tune with a new style or new words | Score-first | [`POST /api/v1/music/v3/edit`](/api-reference/music/score-first#record-a-score) with `style` / `lyrics` |
| You already have the notation | Score-first | `POST /api/v1/music/v3/generate` with `abc`, or `/music/v3/edit` |
When in doubt, use Song mode.
## The Score-first loop
```text theme={null}
plan ──► revise (plain words, repeat) ──► edit (record)
165 credits 165 credits each ~16.5 credits per second of score
```
1. **Write** a score with [`/music/v3/plan`](/api-reference/music/score-first#write-a-score):
either a `description` (AudioPod writes the lyrics, sized to
an optional `target_seconds`) or your own `lyrics` with a `style`. It returns in about ten
seconds with a score and no audio.
2. **Revise** it with [`/music/v3/revise`](/api-reference/music/score-first#revise-a-score-in-plain-words):
"make the chorus brighter", "take it down a key", "simpler melody in bars
5–8". Each revision is checked so that everything you did not ask to change
stays the same, and returns a new score job you can revise again.
3. **Record** it with [`/music/v3/edit`](/api-reference/music/score-first#record-a-score)
when you are happy with it, sending the latest score from
`GET /music/v3/jobs/{job_id}/score`.
Every step is prepaid and refunded if it fails. Song length in Score-first mode
follows the amount of lyrics, so `target_seconds` is a target rather than a
promise — use Song mode when you need an exact length.
Plain-English revision (`/music/v3/revise`) is available when it is enabled
for your account — `GET /api/v1/music/v3/availability` returns
`"revise": true`. Otherwise the route answers `404`; you can still write a
score and record it.
## For agents
The AudioPod MCP server exposes the same choice as tools: `generate_music`
(Song, the default), and `compose_music` → `revise_music_score` →
`record_music_score` (Score-first), with `get_music_score` to read a score for
free. See [MCP server](/sdks/mcp).
# Music Generation
Source: https://docs.audiopod.ai/api-reference/music/generation
Generate songs, instrumentals, rap, vocals, samples, and more using AudioMusic V2 endpoints.
## Text-to-Music
### `POST /api/v1/music/text2music`
Generate complete musical compositions with vocals from text prompts and lyrics.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Generate a pop song with lyrics
job = client.music.song(
prompt="upbeat pop, female vocals, catchy melody, electronic beats",
lyrics="""[Verse 1]
Walking down the street on a sunny day
Everything feels right in every way
[Chorus]
This is our time to shine
Leave the worries behind
Dancing through the night
Everything's gonna be alright""",
duration=120,
wait_for_completion=True
)
print(f"Music URL: {job['output_url']}")
# Advanced generation with AudioMusic V2 parameters
job = client.music.generate(
prompt="epic orchestral soundtrack with dramatic crescendos",
task="text2music",
duration=180,
inference_steps=64,
guidance_scale=10.0,
thinking=True,
generate_lrc=True,
format="flac",
wait_for_completion=True
)
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Generate a pop song with lyrics
const job = await client.music.song(
'upbeat pop, female vocals, catchy melody',
`[Verse 1]
Walking down the street on a sunny day
Everything feels right in every way
[Chorus]
This is our time to shine
Leave the worries behind`,
120
);
const completed = await client.music.waitForCompletion(job.id);
console.log(`Music URL: ${completed.output_url}`);
// Advanced generation with AudioMusic V2 parameters
const advancedJob = await client.music.create({
prompt: 'epic orchestral soundtrack with dramatic crescendos',
task: 'text2music',
duration: 180,
inferenceSteps: 64,
guidanceScale: 10.0,
thinking: true,
generateLrc: true,
format: 'flac',
});
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/text2music" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"caption": "upbeat pop, female vocals, catchy melody, electronic beats",
"lyrics": "[Verse 1]\nWalking down the street on a sunny day\nEverything feels right\n[Chorus]\nThis is our time to shine",
"audio_duration": 120,
"inference_steps": 64,
"guidance_scale": 7.0,
"format": "flac",
"generate_lrc": true
}'
```
**Request Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `caption` | string | Yes | Text description of desired music style (0-2048 chars). Also accepts `prompt` |
| `lyrics` | string | No | Song lyrics with structure tags: `[verse]`, `[chorus]`, `[bridge]`, `[intro]`, `[outro]`, `[Instrumental]` |
| `instrumental` | bool | No | If true, ignores lyrics and generates instrumental. Default: false |
| `genre_preset` | string | No | Genre preset name (see [Overview](/api-reference/music/overview#genre-presets)) |
| `display_name` | string | No | Custom name for the track |
| + all [base parameters](/api-reference/music/overview#complete-parameters-reference) | | | |
**Response:** `MusicGenerationResponse` with `job` object and `message`.
***
## Instrumental Generation
### `POST /api/v1/music/prompt2instrumental`
Create instrumental tracks without vocals from text prompts.
```python theme={null}
client = AudioPod(api_key="ap_your_api_key")
job = client.music.instrumental(
prompt="chill jazz instrumental with piano and saxophone",
duration=90,
wait_for_completion=True
)
print(f"Instrumental URL: {job['output_url']}")
```
```typescript theme={null}
const job = await client.music.instrumental(
'chill jazz instrumental with piano and saxophone',
90
);
const completed = await client.music.waitForCompletion(job.id);
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/prompt2instrumental" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "chill jazz instrumental with piano and saxophone",
"audio_duration": 90,
"format": "flac"
}'
```
**Request Parameters:**
| Parameter | Type | Required | Description |
| - | - | - | - |
| `prompt` | string | Yes | Text description of desired instrumental |
| `genre_preset` | string | No | Genre preset name |
| `display_name` | string | No | Custom name for the track |
| + all [base parameters](/api-reference/music/overview#complete-parameters-reference) | | | |
***
## Rap Generation
### `POST /api/v1/music/text2rap`
Generate rap music with custom lyrics and beats.
```python theme={null}
client = AudioPod(api_key="ap_your_api_key")
job = client.music.rap(
prompt="hard-hitting trap beat with heavy bass and 808s",
lyrics="""[Verse 1]
Started from the bottom now we here
Grinding every day throughout the year
Making moves, breaking through the ceiling
Got that feeling, no time for healing
[Chorus]
This is our time to shine
Leaving all the doubt behind
Every step we take is gold
Watch the story unfold""",
duration=120,
wait_for_completion=True
)
print(f"Rap URL: {job['output_url']}")
```
```typescript theme={null}
const job = await client.music.rap(
'hard-hitting trap beat with heavy bass and 808s',
`[Verse 1]
Started from the bottom now we here
Grinding every day throughout the year
[Chorus]
This is our time to shine
Leaving all the doubt behind`,
120
);
const completed = await client.music.waitForCompletion(job.id);
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/text2rap" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "hard-hitting trap beat with heavy bass and 808s",
"lyrics": "[Verse 1]\nStarted from the bottom now we here\nGrinding every day throughout the year\n[Chorus]\nThis is our time to shine",
"audio_duration": 120
}'
```
**Request Parameters:** Same as Text-to-Music. Lyrics are recommended for best results.
***
## Vocals Generation
### `POST /api/v1/music/lyric2vocals`
Generate vocal tracks from lyrics without full instrumental backing.
```python theme={null}
job = client.music.generate(
prompt="female pop vocals with emotional delivery",
task="lyric2vocals",
lyrics="[Verse 1]\nWalking through the city lights\nEverything feels so bright",
duration=90,
wait_for_completion=True
)
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/lyric2vocals" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "female pop vocals with emotional delivery",
"lyrics": "[Verse 1]\nWalking through the city lights\nEverything feels so bright",
"audio_duration": 90
}'
```
***
## Sample Generation
### `POST /api/v1/music/text2samples`
Create audio loops, one-shots, and samples for music production.
```python theme={null}
job = client.music.generate(
prompt="analog drum loop with vintage character, 120 BPM",
task="text2samples",
duration=8,
wait_for_completion=True
)
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/text2samples" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "analog drum loop with vintage character, 120 BPM",
"audio_duration": 8,
"format": "wav"
}'
```
**Additional Parameters:**
| Parameter | Type | Default | Description |
| - | - | - | - |
| `sample_type` | string | null | Type of sample: `loop`, `one-shot` |
| `tempo` | int | null | Target tempo in BPM (60-200) |
**Note:** Maximum duration for samples is 120 seconds.
***
## Simple Mode
### `POST /api/v1/music/simple`
The easiest way to generate music. Describe what you want in natural language and AudioMusic V2 automatically generates the caption, lyrics, BPM, key, time signature, and duration.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Just describe what you want
job = client.music.simple(
query="energetic K-pop dance track with catchy hooks and a powerful chorus",
wait_for_completion=True
)
print(f"Music URL: {job['output_url']}")
# Instrumental with language preference
job = client.music.simple(
query="epic cinematic trailer music with orchestral elements",
instrumental=True,
wait_for_completion=True
)
# Specify language for vocals
job = client.music.simple(
query="a romantic ballad about lost love",
vocal_language="ja", # Japanese
wait_for_completion=True
)
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
const job = await client.music.simple({
query: 'energetic K-pop dance track with catchy hooks',
});
const completed = await client.music.waitForCompletion(job.id);
// Instrumental
const instrumental = await client.music.simple({
query: 'epic cinematic trailer music',
instrumental: true,
});
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/simple" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "energetic K-pop dance track with catchy hooks and a powerful chorus"
}'
```
**Request Parameters:**
| Parameter | Type | Required | Default | Description |
| - | - | - | - | - |
| `query` | string | Yes | — | Natural language description of desired music (1-1000 chars) |
| `instrumental` | bool | No | false | Generate instrumental only |
| `vocal_language` | string | No | `"unknown"` | Vocal language code (auto-detected if `"unknown"`) |
| `display_name` | string | No | null | Custom name for the track |
| `duration` | float | No | -1 | Duration in seconds (-1 for auto) |
| `format` | string | No | `"flac"` | Output format |
**How Simple Mode Works:**
AudioMusic V2 uses Chain-of-Thought reasoning to automatically:
1. Generate a detailed caption/tags from your natural language query
2. Write lyrics (unless instrumental)
3. Determine optimal BPM, musical key, and time signature
4. Estimate appropriate duration
This makes Simple Mode ideal for non-musicians who want to quickly generate high-quality music.
***
## Response Format
All generation endpoints return the same response structure:
```json theme={null}
{
"job": {
"id": 123,
"task": "text2music",
"status": "PENDING",
"progress": null,
"output_url": null,
"output_urls": null,
"display_name": "My Pop Song",
"audio_duration": 120.0,
"created_at": "2026-01-15T10:30:00Z",
"user_id": "user_123"
},
"message": "Text-to-music generation started"
}
```
**Completed Job** (poll `GET /api/v1/music/jobs/{id}/status`):
```json theme={null}
{
"id": 123,
"task": "text2music",
"status": "COMPLETED",
"progress": 100,
"output_url": "https://media.audiopod.ai/music/123.flac",
"output_urls": {
"flac": "https://media.audiopod.ai/music/123.flac",
"mp3": "https://media.audiopod.ai/music/123.mp3",
"wav": "https://media.audiopod.ai/music/123.wav"
},
"audio_duration": 120.5,
"actual_seeds": [12345],
"lrc_content": "[00:00.00] Walking down the street...",
"quality_score": 0.87,
"display_name": "My Pop Song",
"created_at": "2026-01-15T10:30:00Z",
"completed_at": "2026-01-15T10:32:15Z"
}
```
# Job Management
Source: https://docs.audiopod.ai/api-reference/music/jobs
Query, filter, manage, and retry music generation jobs.
## Get Job Status
### `GET /api/v1/music/jobs/{job_id}/status`
Get the current status of a music generation job.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
job = client.music.get_job(123)
print(f"Status: {job['status']}")
if job['status'] == 'COMPLETED':
print(f"Audio URL: {job['output_url']}")
print(f"Duration: {job['audio_duration']}s")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
const job = await client.music.get(123);
console.log(`Status: ${job.status}`);
if (job.status === 'COMPLETED') {
console.log(`Audio URL: ${job.output_url}`);
}
```
```bash theme={null}
curl "https://api.audiopod.ai/api/v1/music/jobs/123/status" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Response:** Full `MusicGenerationJob` object (see [response format](/api-reference/music/generation#response-format)).
***
## List Jobs
### `GET /api/v1/music/jobs`
List music generation jobs with filtering, search, and sorting.
```python theme={null}
# List recent jobs
jobs = client.music.list_jobs(limit=20)
# Filter by task type
songs = client.music.list_jobs(task="text2music", limit=50)
```
```typescript theme={null}
// List recent jobs
const jobs = await client.music.list({ limit: 20 });
// Filter by status
const completed = await client.music.list({
status: 'COMPLETED',
limit: 50,
});
```
```bash theme={null}
# List with filters
curl "https://api.audiopod.ai/api/v1/music/jobs?status=COMPLETED&sort_by=created_at&sort_order=desc&limit=20" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Query Parameters:**
| Parameter | Type | Default | Description |
| - | - | - | - |
| `skip` | int | 0 | Number of records to skip |
| `limit` | int | 50 | Number of records to return (max 100) |
| `task` | string | null | Filter by task type (`text2music`, `prompt2instrumental`, etc.) |
| `status` | string | null | Filter by status (`PENDING`, `PROCESSING`, `COMPLETED`, `FAILED`) |
| `song_type` | string | null | Filter by library category: `song`, `instrumental`, `edits`, `scores` (or the narrower `rap`, `vocals`, `samples`). See [Library categories](#library-categories) |
| `liked` | bool | null | Filter to liked (true) or not-liked (false) tracks |
| `search` | string | null | Search across display name, prompt, and lyrics |
| `language` | string | null | Filter by vocal language |
| `genre` | string | null | Filter by genre |
| `emotion` | string | null | Filter by emotion tag |
| `occasion` | string | null | Filter by occasion tag |
| `sort_by` | string | `"created_at"` | Sort field: `created_at`, `display_name`, `audio_duration`, `status` |
| `sort_order` | string | `"desc"` | Sort direction: `asc` or `desc` |
| `include_interactions` | bool | false | Include like/dislike status for each job |
**Response Headers:**
* `X-Total-Count`: Total number of matching records (for pagination)
### Library categories
Every job belongs to exactly one of four categories, so `song` + `instrumental` + `edits` + `scores` always adds up to your whole library. The same rule drives `song_type` here, on `/liked` and `/jobs/counts`, and the counts in `/filters/stats`.
| `song_type` | What it returns |
| - | - |
| `song` | Tracks with vocals: `text2music` and `text2rap` with lyrics to sing, and every `lyric2vocals` job |
| `instrumental` | `prompt2instrumental` and `text2samples` jobs, plus `text2music` / `text2rap` jobs generated as instrumentals: `instrumental: true`, no lyrics, lyrics of just `[Instrumental]`, or lyrics made only of section tags such as `[Intro]` |
| `edits` | Jobs made from an existing track: `retake`, `repaint`, `extend`, `edit`, `cover`, `lego`, `extract`, `complete` |
| `scores` | Written scores without a recording: score-only (plan) jobs, and generations that kept their score but finished without audio |
| `rap` | The `text2rap` jobs within `song` |
| `vocals` | The `lyric2vocals` jobs within `song` |
| `samples` | The `text2samples` jobs within `instrumental` |
`song` used to mean "every `text2music` job", and `instrumental` meant only `prompt2instrumental`. A `text2music` track generated as an instrumental is now in `instrumental`, rap tracks with vocals are in `song`, and edits and scores have their own categories. Filter by `task` to get the old task-only behaviour.
***
## Delete Job
### `DELETE /api/v1/music/jobs/{job_id}`
Delete a music generation job and its associated audio files.
```python theme={null}
result = client.music.delete_job(123)
print(result['message']) # "Job deleted successfully"
```
```typescript theme={null}
await client.music.delete(123);
```
```bash theme={null}
curl -X DELETE "https://api.audiopod.ai/api/v1/music/jobs/123" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
***
## Update Job Metadata
### `PATCH /api/v1/music/jobs/{job_id}`
Update a job's display name or thumbnail URL.
```bash theme={null}
curl -X PATCH "https://api.audiopod.ai/api/v1/music/jobs/123" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"display_name": "My Awesome Track",
"thumbnail_url": "https://example.com/thumbnail.jpg"
}'
```
**Request Body:**
| Parameter | Type | Description |
| - | - | - |
| `display_name` | string | New display name |
| `thumbnail_url` | string | New thumbnail image URL |
***
## Retry Failed Job
### `POST /api/v1/music/jobs/{job_id}/retry`
Retry a failed music generation job without consuming additional credits. The original credit reservation is reused.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/jobs/123/retry" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Constraints:**
* Only works on jobs with `status: "FAILED"`
* Original credit reservation is reused (no additional charge)
* Returns a `MusicGenerationResponse` with the re-queued job
**Rate limit:** 10 requests per minute.
***
## Job Counts
### `GET /api/v1/music/jobs/counts`
Get aggregated counts of your music generation jobs, broken down by task type and song category.
```bash theme={null}
# All counts
curl "https://api.audiopod.ai/api/v1/music/jobs/counts" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
# Filter by status
curl "https://api.audiopod.ai/api/v1/music/jobs/counts?status=COMPLETED" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Query Parameters:**
| Parameter | Type | Description |
| - | - | - |
| `status` | string | Filter by status |
| `song_type` | string | Filter by library category: `song`, `instrumental`, `edits`, `scores` (or `rap`, `vocals`, `samples`). See [Library categories](#library-categories) |
**Response:**
`by_song_type` has one count per [library category](#library-categories). `songs` + `instrumental` + `edits` + `scores` equals `total`; `rap` is the part of `songs` made with `text2rap`.
```json theme={null}
{
"total": 150,
"by_task": {
"text2music": 76,
"prompt2instrumental": 40,
"text2rap": 20,
"retake": 10,
"lyric2vocals": 4
},
"by_song_type": {
"songs": 85,
"instrumental": 50,
"edits": 10,
"scores": 5,
"rap": 18
},
"filters": {
"status": null,
"song_type": null
}
}
```
***
## Liked Tracks
### `GET /api/v1/music/liked`
Get all tracks you've liked.
```bash theme={null}
curl "https://api.audiopod.ai/api/v1/music/liked?limit=20" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Query Parameters:**
| Parameter | Type | Default | Description |
| - | - | - | - |
| `skip` | int | 0 | Pagination offset |
| `limit` | int | 50 | Number of records |
| `song_type` | string | null | Filter by [library category](#library-categories) |
| `status` | string | null | Filter by status |
***
## Filter Statistics
### `GET /api/v1/music/filters/stats`
Get statistics for building filter UI dropdowns — total tracks, liked count, task distribution, and available filter values.
```bash theme={null}
curl "https://api.audiopod.ai/api/v1/music/filters/stats" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Response:**
`song_types` counts each [library category](#library-categories): `songs` + `instrumental` + `edits` + `scores` equals `total_tracks`, and each count matches what `GET /api/v1/music/jobs?song_type=...` returns (`songs` is the `song` filter). `rap` is the part of `songs` made with `text2rap`.
```json theme={null}
{
"total_tracks": 150,
"liked_count": 25,
"song_types": {
"songs": 85,
"instrumental": 50,
"edits": 10,
"scores": 5,
"rap": 18
},
"available_filters": {
"song_types": ["songs", "rap", "instrumental", "edits", "scores"],
"interactions": ["liked"],
"statuses": ["pending", "processing", "completed", "failed"],
"languages": ["en", "zh", "ja", "ko"],
"genres": ["pop", "rock", "jazz"],
"emotions": ["happy", "sad", "energetic"],
"occasions": ["party", "relaxation", "workout"]
}
}
```
***
## Genre Presets
### `GET /api/v1/music/presets`
Get available genre presets with their pre-configured parameters.
```python theme={null}
presets = client.music.get_presets()
for name, config in presets['genre_presets'].items():
print(f"{name}: {config}")
```
```typescript theme={null}
const presets = await client.music.getPresets();
console.log(presets);
```
```bash theme={null}
curl "https://api.audiopod.ai/api/v1/music/presets" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
***
## Model Status
### `GET /api/v1/music/model/status`
Get AudioMusic V2 model health and status information.
```bash theme={null}
curl "https://api.audiopod.ai/api/v1/music/model/status" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
***
## Sample Data
### `GET /api/v1/music/sample-data`
Get sample/test data for development and testing purposes.
```bash theme={null}
curl "https://api.audiopod.ai/api/v1/music/sample-data" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
# AudioMusic V2
Source: https://docs.audiopod.ai/api-reference/music/overview
Generate original music, instrumentals, covers, and audio transformations using AudioPod AI's AudioMusic V2 engine.
## Overview
AudioPod AI's **AudioMusic V2** is a state-of-the-art music generation engine that transforms text descriptions into original musical compositions. Powered by a Transformer Model with Chain-of-Thought reasoning, AudioMusic V2 delivers studio-quality results.
### Key Capabilities
* **Text-to-Music**: Generate complete songs with vocals from prompts and lyrics
* **Simple Mode**: Describe what you want in natural language — AudioMusic V2 handles everything
* **Cover / Style Transfer**: Transform existing audio into different styles and genres
* **Reference Audio**: Guide generation using a reference track's timbre and mixing style
* **Audio Analysis**: Extract metadata (BPM, key, lyrics, caption) from any audio
* **Stem Extraction**: Isolate specific instruments (vocals, drums, bass, guitar, and more)
* **Repaint / Edit / Extend**: Modify specific sections, edit with new prompts, or extend tracks
* **Batch Generation**: Generate 1-8 variations in a single request
* **LRC Timestamps**: Synchronized lyric timestamps for karaoke-style playback
* **Quality Scoring**: Automatic quality assessment for every generation
* **50+ Languages**: Generate music with vocals in over 50 languages
* **Multiple Formats**: Output in WAV, MP3, FLAC, or OGG
* **Royalty-Free**: All generated music is 100% royalty-free for commercial use
## Authentication
All music endpoints require authentication. Use one of these methods:
* **API Key (Recommended)**: `X-API-Key: your_api_key` header
* **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth)
```bash theme={null}
# Example with API Key
curl -X POST "https://api.audiopod.ai/api/v1/music/simple" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "upbeat pop song with catchy melody"}'
```
## Quick Start — Simple Mode
The fastest way to generate music. Just describe what you want:
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Simple mode — AudioMusic V2 auto-generates everything
job = client.music.simple(
query="a soft Bengali love song for a quiet evening",
wait_for_completion=True
)
print(f"Music URL: {job['output_url']}")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Simple mode — AudioMusic V2 auto-generates everything
const job = await client.music.simple({
query: 'a soft Bengali love song for a quiet evening',
});
const completed = await client.music.waitForCompletion(job.id);
console.log(`Music URL: ${completed.output_url}`);
```
```bash theme={null}
# Create job
JOB=$(curl -s -X POST "https://api.audiopod.ai/api/v1/music/simple" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "a soft Bengali love song for a quiet evening"}')
JOB_ID=$(echo $JOB | jq -r '.job.id')
echo "Job created: $JOB_ID"
# Poll for completion
while true; do
STATUS=$(curl -s "https://api.audiopod.ai/api/v1/music/jobs/$JOB_ID/status" \
-H "X-API-Key: $AUDIOPOD_API_KEY")
STATE=$(echo $STATUS | jq -r '.status')
echo "Status: $STATE"
[ "$STATE" = "COMPLETED" ] && echo "URL: $(echo $STATUS | jq -r '.output_url')" && break
[ "$STATE" = "FAILED" ] && echo "Failed" && break
sleep 5
done
```
## Complete Parameters Reference
All music generation endpoints inherit from a common base. Here are all available parameters:
### Core Parameters
| Parameter | Type | Default | Description |
| - | - | - | - |
| `duration` | float | -1.0 | Audio duration in seconds. Use -1 for automatic duration based on lyrics length. Max: 600 |
| `format` | string | `"flac"` | Output audio format: `wav`, `mp3`, `flac`, `ogg` |
| `model_version` | string | `"audiomusic-v2"` | Model version: `audiomusic-v2` (recommended) or `audiomusic-v1.5` |
| `batch_size` | int | 1 | Number of variations to generate (1-8) |
| `seed` | int | -1 | Random seed for reproducibility. Use -1 for random |
### Diffusion Parameters
| Parameter | Type | Default | Description |
| - | - | - | - |
| `inference_steps` | int | 64 | Number of diffusion steps. Higher = better quality, slower. Range: 32-100 |
| `guidance_scale` | float | 7.0 | Classifier-free guidance scale. Higher = more prompt adherence. Range: 1.0-15.0 |
| `shift` | float | 3.0 | Timestep shift factor. Range: 1.0-5.0 |
| `infer_method` | string | `"ode"` | Inference method: `ode` (Euler, faster) or `sde` (stochastic, more diverse) |
| `use_adg` | bool | false | Enable Adaptive Dual Guidance for enhanced control |
| `cfg_interval_start` | float | 0.0 | CFG interval start ratio (0.0-1.0) |
| `cfg_interval_end` | float | 1.0 | CFG interval end ratio (0.0-1.0) |
### Language Model Parameters
AudioMusic V2 includes a language model that can auto-generate and refine metadata, captions, lyrics, and more.
| Parameter | Type | Default | Description |
| - | - | - | - |
| `thinking` | bool | true | Enable Chain-of-Thought reasoning for higher quality |
| `lm_temperature` | float | 0.85 | Sampling temperature (0.0-2.0). Lower = more deterministic |
| `lm_cfg_scale` | float | 2.0 | LM classifier-free guidance (0.0-10.0) |
| `lm_top_k` | int | 0 | Top-k sampling (0-100, 0 disables) |
| `lm_top_p` | float | 0.9 | Nucleus sampling threshold (0.0-1.0) |
| `lm_negative_prompt` | string | `"NO USER INPUT"` | Negative prompt for LM guidance |
| `use_cot_metas` | bool | true | Auto-generate BPM, key, time signature via Chain-of-Thought |
| `use_cot_caption` | bool | true | Refine caption/tags via Chain-of-Thought |
| `use_cot_lyrics` | bool | false | Generate or refine lyrics via Chain-of-Thought |
| `use_cot_language` | bool | true | Auto-detect vocal language via Chain-of-Thought |
| `use_constrained_decoding` | bool | false | Enable structured LM output |
### Music Metadata Parameters
These are auto-detected when `thinking: true`, but can be specified manually.
| Parameter | Type | Default | Description |
| - | - | - | - |
| `bpm` | int | null | Beats per minute (30-300). Auto-detected if null |
| `keyscale` | string | null | Musical key, e.g. `"C Major"`, `"Am"`. Auto-detected if null |
| `timesignature` | string | null | Time signature: `"2/4"`, `"3/4"`, `"4/4"`, `"6/8"`. Auto-detected if null |
| `vocal_language` | string | `"unknown"` | ISO 639-1 language code (e.g. `"en"`, `"zh"`, `"ja"`). Use `"unknown"` for auto-detection |
### Output & Quality Parameters
| Parameter | Type | Default | Description |
| - | - | - | - |
| `generate_lrc` | bool | false | Generate synchronized LRC lyric timestamps |
| `calculate_quality` | bool | true | Calculate quality score (0.0-1.0) for the output |
| `thumbnail_url` | string | null | URL for custom track thumbnail image |
### LoRA Parameters
| Parameter | Type | Default | Description |
| - | - | - | - |
| `lora_name_or_path` | string | null | Path to LoRA adapter for fine-tuned styles |
| `lora_weight` | float | 1.0 | LoRA adapter weight (-3.0 to 3.0) |
## Genre Presets
Pre-configured genre settings for quick generation:
| Preset | Description |
| - | - |
| `Modern Pop` | Contemporary pop with electronic elements |
| `Rock` | Guitar-driven rock music |
| `Hip Hop` | Modern hip hop beats and production |
| `Jazz` | Jazz compositions with improvisation |
| `Classical` | Orchestral and classical arrangements |
| `Electronic` | EDM, synth-driven electronic music |
| `Country` | Country music with acoustic instruments |
| `Folk` | Folk music with traditional elements |
| `Blues` | Blues with soulful expression |
| `Reggae` | Reggae with Caribbean rhythms |
| `Latin` | Latin music styles |
| `R&B` | Rhythm and blues |
| `Metal` | Heavy metal and rock subgenres |
| `Custom` | Custom genre (specify in caption/prompt) |
## Output Formats
| Format | Extension | Quality | Use Case |
| - | - | - | - |
| FLAC | `.flac` | Lossless | Default. Best quality, larger files |
| WAV | `.wav` | Lossless | Uncompressed, maximum compatibility |
| MP3 | `.mp3` | Lossy | Smaller files, streaming |
| OGG | `.ogg` | Lossy | Open format, good compression |
## Job Lifecycle
All music generation is asynchronous:
1. **Submit** a generation request → receive a job with `status: "PENDING"`
2. **Poll** the job status endpoint → `"PROCESSING"` while generating
3. **Complete** → `status: "COMPLETED"` with `output_url` and `output_urls` (multiple formats)
4. **Failed** → `status: "FAILED"` with `error_message` (use retry endpoint to re-queue)
## Pricing
Music generation is billed per minute of audio generated. Two billing paths
apply depending on how you authenticate:
| Path | Rate |
| - | - |
| Account credits (subscription / JWT) | **990 credits/minute** |
| API wallet (API key, USD-denominated) | **\$0.04/minute** |
| Duration | Credits | API wallet (USD) |
| - | - | - |
| 30 seconds | 495 | \$0.02 |
| 1 minute | 990 | \$0.04 |
| 2 minutes | 1,980 | \$0.08 |
| 5 minutes | 4,950 | \$0.20 |
| 10 minutes | 9,900 | \$0.40 |
**AudioMusic Premium** (`quality: "premium"`) is a higher-fidelity variant
that bills at **2× the base rate** — 1,980 credits/minute or \$0.08/minute.
Requires a Pro-tier or higher subscription; calls from Free/Basic tiers return
`402 PREMIUM_TIER_REQUIRED`.
Credits are reserved when the job is created and refunded if generation fails.
## Best Practices
1. **Use Simple Mode for quick results** — describe what you want in natural language and let AudioMusic V2 handle the details
2. **Enable `thinking: true`** (default) for best quality — Chain-of-Thought reasoning significantly improves output
3. **Use `caption` over `prompt`** — AudioMusic V2 uses `caption` internally; `prompt` is a legacy alias
4. **Set `inference_steps: 64`** for production quality. Use 32 for fast drafts, 8 for turbo previews
5. **Default `guidance_scale: 7.0`** works well for most cases. Increase to 10-15 for stronger prompt adherence
6. **Use structure tags in lyrics** — `[verse]`, `[chorus]`, `[bridge]`, `[intro]`, `[outro]`, `[Instrumental]`
7. **Use `batch_size > 1`** to generate multiple variations and pick the best one
8. **Enable `generate_lrc: true`** when you need synchronized lyric timestamps
## Error Handling
| Status Code | Description |
| - | - |
| 200 | Success |
| 400 | Invalid parameters (check error message for details) |
| 401 | Authentication required |
| 402 | Insufficient credits |
| 404 | Job or resource not found |
| 429 | Rate limit exceeded (wait and retry) |
| 500 | Internal server error |
**"Insufficient credits"** — Your wallet balance is too low. Top up at [Dashboard](https://www.audiopod.ai/dashboard/account).
**"Job must be completed"** — You tried to retake/extend/edit a job that hasn't finished yet. Wait for completion first.
**"Original music job not found"** — The source job ID doesn't exist or doesn't belong to your account.
**"Rate limit exceeded"** — You've exceeded the rate limit. Wait a moment and try again.
## Next Steps
Text-to-Music, Instrumental, Rap, Vocals, Samples, Simple Mode
Cover, Reference Audio, Analysis, Stem Extraction, Edit, Extend
Job status, listing, filtering, retry, presets
# Score-first music
Source: https://docs.audiopod.ai/api-reference/music/score-first
Write an editable score, revise it in plain words, and record it — the AudioMusic Score-first endpoints.
Score-first is the AudioMusic mode for shaping a song before it is recorded.
Every job here returns a score (melody, chords and sections, as ABC notation
plus MIDI) alongside — or, for plan and revise jobs, instead of — the audio.
Not sure which mode you need? See [Choosing how to make music](/api-reference/music/choosing).
All routes live under `/api/v1/music/v3` and take the usual `X-API-Key`
header. Check `GET /api/v1/music/v3/availability` before showing a Score-first
option in your own product:
```json theme={null}
{ "available": true, "revise": true }
```
`available` says whether Score-first is offered; `revise` whether
plain-English revision is enabled for your account.
## Pricing
| Step | Endpoint | Credits |
| - | - | - |
| Write a score (lyrics included when you send a description) | `POST /music/v3/plan` | **165** flat |
| Revise a score | `POST /music/v3/revise` | **165** flat per revision |
| Record a score | `POST /music/v3/edit` | \~**16.5 per second** of the score's length (15 s minimum, 600 s maximum) |
| Read a job or score | `GET /music/v3/jobs/{job_id}`, `/score` | free |
Credits are reserved when you call and refunded if the job fails or the
request is refused.
## Write a score
### `POST /api/v1/music/v3/plan`
Writes a score only — no audio. Send a `description` and AudioPod writes the
lyrics first, within the same charge; or send your own `lyrics`.
| Field | Type | Required | Description |
| - | - | - | - |
| `description` | string | one of | What the song is about — topic, mood, genre (1–1000 characters). AudioPod writes the lyrics. Cannot be combined with `lyrics` |
| `lyrics` | string | one of | Your own lyrics; section tags such as `[verse]` and `[chorus]` help |
| `style` | string | with `lyrics` | Genre, instrumentation, mood, tempo feel, vocal character (up to 2000 characters). Required with `lyrics`; with a `description` it defaults to the description |
| `target_seconds` | integer | no | 30–300. Roughly how long the song should be; used to size the lyrics AudioPod writes. A guide, not a guarantee |
| `language` | string | no | Language to write the lyrics in (ISO 639-1, e.g. `en`, `hi`). Defaults to the language of the description, else English |
| `cot` | string | no | `full` (default) writes melody and harmony; `melody` the tune alone |
| `seed` | integer | no | Reuse to get closer to an earlier result |
| `display_name` | string | no | Your name for the track |
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/v3/plan" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"style": "warm acoustic folk, fingerpicked guitar, gentle male vocal",
"description": "a lighthouse keeper waiting for the ships to come home",
"target_seconds": 120
}'
```
The response is a [job](#job-response) with `workflow: "plan"`, plus:
* `lyrics_source` — `audiopod` (written from your description) or `user`
(you supplied them); absent for instrumentals.
* `target_seconds` — the length you asked for, if any.
* `score.duration_seconds` — the score's own length once it is written. Song
length follows the amount of lyrics, so this can differ from
`target_seconds`; use Song mode when you need an exact length.
| Status | `detail.code` | Meaning |
| - | - | - |
| `422` | `DESCRIPTION_NOT_ALLOWED` | The description asks for the words of an existing song. AudioPod writes original songs; describe the mood, genre and topic instead. Nothing is charged |
| `422` | `LYRICS_FAILED` | Lyrics could not be written for that description. Nothing is charged; rephrase it or send your own lyrics |
| `503` | `LYRICS_BUSY` | Lyric writing is busy; retry after `Retry-After` seconds |
## Revise a score in plain words
### `POST /api/v1/music/v3/revise`
Available when plain-English revision is enabled for your account
(`revise: true` from `/availability`); otherwise this route answers `404`.
Applies a change described in words and returns a **new** score-only job
linked to the one you revised; the original is kept. Parts of the score you
did not ask about are kept exactly as they were, and the tempo only changes
when `allow_tempo_change` is true. Revise as often as you like, then record
the version you want.
| Field | Type | Required | Description |
| - | - | - | - |
| `source_job_id` | integer | yes | The score to change: a plan, revise or recorded job you own that has a score |
| `instruction` | string | yes | The change, in plain words (1–500 characters) — "make the chorus higher", "add a bridge after the second chorus" |
| `section` | string | no | Limit the change to one section by its label (up to 60 characters): `chorus` for every chorus, `chorus 2` for the second |
| `bars` | object | no | Limit the change to a bar range, numbered from 1, both ends included: `{"start": 5, "end": 8}` (1–10000) |
| `allow_tempo_change` | boolean | no | Permit the revision to change the tempo. Off by default |
| `display_name` | string | no | Your name for the revised score |
Send an **`Idempotency-Key`** header (up to 200 characters) to make a retry
safe: the same key from you within 10 minutes returns the first result and is
not charged again.
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/v3/revise" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Idempotency-Key: 2f6c1d7e-revise-chorus" \
-H "Content-Type: application/json" \
-d '{"source_job_id": 4301, "instruction": "make the chorus higher", "section": "chorus"}'
```
The job has `workflow: "revise"` and its `source_job_id`. `note` says in one
sentence what changed, and `score_comparison.changed_bars` says where — the
changed bar ranges per voice.
Errors — nothing is charged for any of these:
| Status | `detail.code` | Meaning |
| - | - | - |
| `404` | — | The source is not yours or has no score, or revision is not enabled for your account |
| `409` | `REVISE_IN_FLIGHT` | The same change to the same score is already being made; it will appear in your library shortly |
| `402` | — | Not enough credits |
| `422` | `REVISE_TARGET_NOT_FOUND` | The score has no such section or bars; the message lists what it has |
| `422` | `REVISE_FAILED` | The change could not be made as a valid score. Describe it differently, or narrow it with `section` / `bars` |
| `422` | `INVALID_SCORE` | The source score itself cannot be read |
| `422` | `IDEMPOTENCY_KEY_REUSED` | The `Idempotency-Key` was already used with a different request body |
| `429` | `REVISE_RATE_LIMITED` | Too many revisions (20 a minute, 300 a day) or several failed in a row; wait `Retry-After` seconds |
| `503` | `REVISE_UNAVAILABLE` | Briefly unavailable; retry shortly |
## Record a score
### `POST /api/v1/music/v3/edit`
Records a score as a finished track. Send the score to perform — usually the
latest revision's score from `GET /music/v3/jobs/{job_id}/score` — and
optionally a new style or new words to perform the same tune differently.
| Field | Type | Required | Description |
| - | - | - | - |
| `source_job_id` | integer | yes | The job the score came from |
| `abc` | string | yes | The score to record, in ABC notation |
| `style` | string | no | A new style for the same tune; defaults to the source's |
| `lyrics` | string | no | New words for the same tune; defaults to the source's |
| `cot` | string | no | `melody` (default) or `full` |
| `allow_tempo_change` | boolean | no | Permit the score to change the source's tempo |
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/music/v3/edit" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"source_job_id": 4302, "abc": "X:1\n..."}'
```
A recording is a new performance of the whole piece: sections you did not
change will still sound different from an earlier take. An identical request
for the same score while one is still recording is refused with `409`
(`SCORE_ALREADY_RECORDING`, with the `job_id` already in progress) and nothing
is charged.
## Jobs
| Endpoint | Returns |
| - | - |
| `GET /api/v1/music/v3/jobs/{job_id}` | One job: status, audio, score, comparison and billing |
| `GET /api/v1/music/v3/jobs/{job_id}/score` | Just the score, ready to revise or record |
| `GET /api/v1/music/v3/jobs` | Your Score-first jobs, newest first |
| `GET /api/v1/music/v3/availability` | `{"available": true, "revise": true}` |
### Job response
```json theme={null}
{
"job_id": 4302,
"workflow": "revise",
"status": "COMPLETED",
"style": "warm acoustic folk, fingerpicked guitar, gentle male vocal",
"lyrics": "[verse]\n...",
"lyrics_source": "audiopod",
"audio_url": null,
"score": {
"abc": "X:1\n...",
"midi_url": "https://...",
"editable": true,
"duration_seconds": 118.0
},
"score_comparison": { "changed_bars": { "Vocal": [[10, 10]], "Ins": [] } },
"source_job_id": 4301,
"note": "Changed the melody in chorus 1 (bar 10); everything else is unchanged."
}
```
`audio_url` is `null` for plan and revise jobs — they are scores. Poll until
`status` is `COMPLETED` or `FAILED`.
# Noise Reduction
Source: https://docs.audiopod.ai/api-reference/noise-reduction
Remove background noise, enhance audio quality, and clean up recordings using AudioPod AI's advanced noise reduction technology.
## Overview
AudioPod AI's Noise Reduction API uses advanced AI models to remove background noise, enhance speech clarity, and improve overall audio quality. Process recordings to eliminate unwanted sounds while preserving the natural characteristics of speech and music.
### Key Features
* **AI-Powered Denoising**: Advanced neural networks for intelligent noise removal
* **Three Quality Modes**: `balanced` (faster, lighter), `studio` (the default) and `ultra` (highest fidelity)
* **Spectral Processing**: Traditional spectral subtraction for specific noise types
* **Voice Preservation**: Maintain natural speech characteristics during cleanup
* **Video Support**: Process audio from video files while preserving video streams
* **Batch Processing**: Handle multiple files efficiently
## Authentication
All endpoints require authentication. Use one of these methods:
* **API Key (Recommended)**: `X-API-Key: your_api_key` header
* **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth)
## AI Noise Reduction
### Standard Denoising
Remove background noise using AI-powered denoising models.
```python theme={null}
from audiopod import Client
client = Client()
# Simple noise reduction
denoised = client.denoiser.denoise_audio(
audio_file="noisy_recording.wav",
quality_mode="balanced", # "balanced", "studio" (default) or "ultra"
wait_for_completion=True
)
print(f"Denoising completed!")
print(f"Input file: {denoised.input_file}")
print(f"Denoised audio URL: {denoised.denoised_audio_url}")
print(f"Noise reduction level: {denoised.noise_reduction_db}dB")
# Download cleaned audio
clean_audio = client.download_file(denoised.denoised_audio_url)
with open("clean_recording.wav", "wb") as f:
f.write(clean_audio)
# Studio quality denoising
studio_denoised = client.denoiser.denoise_audio(
audio_file="interview_with_background_noise.wav",
quality_mode="studio", # Higher quality processing
wait_for_completion=True
)
print(f"Studio denoising completed!")
print(f"Noise reduction level: {studio_denoised.noise_reduction_db}dB")
# Batch processing multiple files
noisy_files = [
"meeting_room_recording.wav",
"interview_outdoors.mp3",
"podcast_home_setup.wav",
"lecture_hall_audio.m4a"
]
batch_results = client.denoiser.denoise_batch(
audio_files=noisy_files,
quality_mode="balanced",
wait_for_completion=True
)
for i, result in enumerate(batch_results):
print(f"\nFile {i+1}: {noisy_files[i]}")
print(f"Status: {result.status}")
if result.status == "completed":
print(f"Noise reduced: {result.noise_reduction_db}dB")
print(f"Clean audio: {result.denoised_audio_url}")
# Download and save
clean_audio = client.download_file(result.denoised_audio_url)
output_name = f"clean_{i+1}_{noisy_files[i].split('.')[0]}.wav"
with open(output_name, "wb") as f:
f.write(clean_audio)
print(f"Saved: {output_name}")
# Process video files (audio cleaning)
video_denoised = client.denoiser.denoise_video(
video_file="presentation_with_noise.mp4",
quality_mode="studio", # Higher quality processing
preserve_video=True, # Keep original video stream
wait_for_completion=True
)
print(f"Video denoising completed!")
print(f"Clean video URL: {video_denoised.video_output_url}")
print(f"Audio-only URL: {video_denoised.denoised_audio_url}")
```
```javascript theme={null}
// Note: SDK examples shown are conceptual and may not reflect the current SDK implementation
// For production use, refer to the Raw HTTP examples or check the latest SDK documentation
const { AudioPodClient } = require('audiopod-js');
const fs = require('fs');
const client = new AudioPodClient();
async function denoiseAudio() {
try {
// Simple noise reduction
const denoised = await client.denoiser.denoiseAudio({
audioFile: fs.createReadStream('noisy_recording.wav'),
qualityMode: 'balanced', // "balanced", "studio" (default) or "ultra"
waitForCompletion: true
});
console.log('Denoising completed!');
console.log(`Input file: ${denoised.inputFile}`);
console.log(`Denoised audio URL: ${denoised.denoisedAudioUrl}`);
console.log(`Noise reduction level: ${denoised.noiseReductionDb}dB`);
// Download cleaned audio
const cleanAudio = await client.downloadFile(denoised.denoisedAudioUrl);
fs.writeFileSync('clean_recording.wav', cleanAudio);
// Studio quality denoising
const studioDenoised = await client.denoiser.denoiseAudio({
audioFile: fs.createReadStream('interview_with_background_noise.wav'),
qualityMode: 'studio', // Higher quality processing
waitForCompletion: true
});
console.log('Studio denoising completed!');
console.log(`Noise reduction level: ${studioDenoised.noiseReductionDb}dB`);
// Batch processing multiple files
const noisyFiles = [
'meeting_room_recording.wav',
'interview_outdoors.mp3',
'podcast_home_setup.wav',
'lecture_hall_audio.m4a'
];
const fileStreams = noisyFiles.map(file => ({
file: fs.createReadStream(file),
name: file
}));
const batchResults = await client.denoiser.denoiseBatch({
audioFiles: fileStreams,
qualityMode: 'balanced',
waitForCompletion: true
});
batchResults.forEach((result, index) => {
console.log(`\nFile ${index + 1}: ${noisyFiles[index]}`);
console.log(`Status: ${result.status}`);
if (result.status === 'completed') {
console.log(`Noise reduced: ${result.noiseReductionDb}dB`);
console.log(`Clean audio: ${result.denoisedAudioUrl}`);
// Download and save
client.downloadFile(result.denoisedAudioUrl).then(cleanAudio => {
const outputName = `clean_${index + 1}_${noisyFiles[index].split('.')[0]}.wav`;
fs.writeFileSync(outputName, cleanAudio);
console.log(`Saved: ${outputName}`);
});
}
});
// Process video files (audio cleaning)
const videoDenoised = await client.denoiser.denoiseVideo({
videoFile: fs.createReadStream('presentation_with_noise.mp4'),
qualityMode: 'studio', // Higher quality processing
preserveVideo: true, // Keep original video stream
waitForCompletion: true
});
console.log('Video denoising completed!');
console.log(`Clean video URL: ${videoDenoised.videoOutputUrl}`);
console.log(`Audio-only URL: ${videoDenoised.denoisedAudioUrl}`);
} catch (error) {
console.error('Denoising error:', error.message);
}
}
denoiseAudio();
```
```python theme={null}
import requests
import time
# Standard denoising
with open("noisy_recording.wav", "rb") as audio_file:
response = requests.post(
"https://api.audiopod.ai/api/v1/denoiser/denoise",
headers={"X-API-Key": api_key},
data={
"quality_mode": "balanced" # "balanced", "studio" (default) or "ultra"
},
files={"file": audio_file}
)
if response.status_code == 200:
job_data = response.json()
job_id = job_data["id"]
print(f"Denoising job created: {job_id}")
# Poll for completion
while True:
status_response = requests.get(
f"https://api.audiopod.ai/api/v1/denoiser/jobs/{job_id}",
headers={"X-API-Key": api_key}
)
if status_response.status_code == 200:
job_status = status_response.json()
print(f"Status: {job_status['status']}")
if job_status["status"] == "COMPLETED":
print(f"Denoising completed!")
print(f"Clean audio URL: {job_status.get('denoised_audio_url')}")
print(f"Noise reduction: {job_status.get('noise_reduction_db', 0)}dB")
# Download the cleaned audio
if job_status.get('denoised_audio_url'):
audio_response = requests.get(job_status['denoised_audio_url'])
with open("clean_recording.wav", "wb") as f:
f.write(audio_response.content)
print("Downloaded clean audio: clean_recording.wav")
break
elif job_status["status"] == "FAILED":
print(f"Denoising failed: {job_status.get('error', 'Unknown error')}")
break
time.sleep(5) # Check every 5 seconds
else:
print(f"Error: {response.status_code} - {response.text}")
```
```bash theme={null}
# Upload file for denoising
curl -X POST "https://api.audiopod.ai/api/v1/denoiser/denoise" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "file=@noisy_recording.wav" \
-F "quality_mode=balanced"
# Check job status (replace JOB_ID with actual job ID)
curl -X GET "https://api.audiopod.ai/api/v1/denoiser/jobs/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
# Studio quality denoising
curl -X POST "https://api.audiopod.ai/api/v1/denoiser/denoise" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "file=@very_noisy_audio.wav" \
-F "quality_mode=studio"
```
### Denoise from URL
Process audio/video from URLs (YouTube, etc.) to remove noise.
```python theme={null}
# Note: SDK examples shown are conceptual and may not reflect the current SDK implementation
# For production use, refer to the Raw HTTP examples or check the latest SDK documentation
from audiopod import Client
client = Client()
# Denoise from YouTube URL
url_denoised = client.denoiser.denoise_from_url(
url="https://youtube.com/watch?v=example123",
quality_mode="studio", # Higher quality processing for online content
wait_for_completion=True
)
print(f"URL denoising completed!")
print(f"Source: {url_denoised.source_url}")
print(f"Clean audio URL: {url_denoised.denoised_audio_url}")
print(f"Noise reduction: {url_denoised.noise_reduction_db}dB")
# Batch URL processing
urls = [
"https://youtube.com/watch?v=lecture1",
"https://vimeo.com/meeting_recording",
"https://example.com/noisy_podcast.mp3"
]
batch_url_results = client.denoiser.denoise_batch_from_urls(
urls=urls,
quality_mode="balanced",
wait_for_completion=True
)
for i, result in enumerate(batch_url_results):
print(f"\nURL {i+1}: {urls[i]}")
print(f"Status: {result.status}")
if result.status == "completed":
print(f"Clean audio: {result.denoised_audio_url}")
```
```javascript theme={null}
// Note: SDK examples shown are conceptual and may not reflect the current SDK implementation
// For production use, refer to the Raw HTTP examples or check the latest SDK documentation
const { AudioPodClient } = require('audiopod-js');
const client = new AudioPodClient();
async function denoiseFromUrl() {
try {
// Denoise from YouTube URL
const urlDenoised = await client.denoiser.denoiseFromUrl({
url: "https://youtube.com/watch?v=example123",
qualityMode: "studio", // Higher quality processing for online content
waitForCompletion: true
});
console.log('URL denoising completed!');
console.log(`Source: ${urlDenoised.sourceUrl}`);
console.log(`Clean audio URL: ${urlDenoised.denoisedAudioUrl}`);
console.log(`Noise reduction: ${urlDenoised.noiseReductionDb}dB`);
// Batch URL processing
const urls = [
"https://youtube.com/watch?v=lecture1",
"https://vimeo.com/meeting_recording",
"https://example.com/noisy_podcast.mp3"
];
const batchUrlResults = await client.denoiser.denoiseBatchFromUrls({
urls: urls,
qualityMode: "balanced",
waitForCompletion: true
});
batchUrlResults.forEach((result, index) => {
console.log(`\nURL ${index + 1}: ${urls[index]}`);
console.log(`Status: ${result.status}`);
if (result.status === "completed") {
console.log(`Clean audio: ${result.denoisedAudioUrl}`);
}
});
} catch (error) {
console.error('URL denoising error:', error.message);
}
}
denoiseFromUrl();
```
```python theme={null}
import requests
import time
response = requests.post(
"https://api.audiopod.ai/api/v1/denoiser/denoise",
headers={"X-API-Key": api_key},
data={
"url": "https://youtube.com/watch?v=example123",
"quality_mode": "studio" # Higher quality processing
}
)
if response.status_code == 200:
job_data = response.json()
job_id = job_data["id"]
print(f"URL denoising started: {job_id}")
# Poll for completion
while True:
status_response = requests.get(
f"https://api.audiopod.ai/api/v1/denoiser/jobs/{job_id}",
headers={"X-API-Key": api_key}
)
if status_response.status_code == 200:
job_status = status_response.json()
print(f"Status: {job_status['status']}")
if job_status["status"] == "COMPLETED":
print(f"URL denoising completed!")
print(f"Source: {job_status.get('source_url')}")
print(f"Clean audio: {job_status.get('denoised_audio_url')}")
break
elif job_status["status"] == "FAILED":
print(f"Denoising failed: {job_status.get('error')}")
break
time.sleep(10) # URL processing may take longer
```
```bash theme={null}
# Denoise from URL (multipart form fields, not a JSON body)
curl -X POST "https://api.audiopod.ai/api/v1/denoiser/denoise" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=example123" \
-F "quality_mode=ultra"
# Check job status
curl -X GET "https://api.audiopod.ai/api/v1/denoiser/jobs/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Quality Modes:**
Every mode removes background noise and improves speech clarity. Send the mode as the `quality_mode` form field.
* **`balanced`**: The fastest, lightest mode, with clean results.
* **`studio`** (default): Broadcast and podcast quality. Used when `quality_mode` is omitted or blank.
* **`ultra`**: The highest fidelity, restoring lost high frequencies.
The value is case-insensitive. Any other value is rejected with a `422` before a job is created or any credits are reserved.
**Response:**
```json theme={null}
{
"id": 123,
"status": "PROCESSING",
"progress": 0,
"input_path": "/uploads/noisy_recording.wav",
"is_video": false,
"task_id": "celery_task_uuid_here",
"created_at": "2024-01-15T10:30:00Z",
"original_filename": "noisy_recording.wav",
"display_name": "noisy_recording.wav"
}
```
## Spectral Noise Reduction
### Advanced Spectral Processing
Use traditional spectral subtraction for specific noise types and fine control.
```http theme={null}
POST /api/v1/denoiser/spectral/denoise
X-API-Key: {api_key}
Content-Type: multipart/form-data
file: (audio file)
noise_type: general
prop_decrease: 0.8
stationary: true
```
```python theme={null}
with open("recording_with_hum.wav", "rb") as audio_file:
response = requests.post(
"https://api.audiopod.ai/api/v1/denoiser/spectral/denoise",
headers={"X-API-Key": api_key},
data={
"noise_type": "electrical_hum", # Specific noise type
"prop_decrease": 0.8, # 80% noise reduction
"stationary": True, # Constant noise
"time_constant_s": 2.0, # Adaptation time
"freq_mask_smooth_hz": 500, # Frequency smoothing
"time_mask_smooth_ms": 50 # Time smoothing
},
files={"file": audio_file}
)
if response.status_code == 200:
job_data = response.json()
print(f"Spectral denoising job: {job_data['id']}")
```
**Noise Types:**
* **`general`**: General background noise
* **`electrical_hum`**: 50/60Hz electrical interference
* **`air_conditioning`**: HVAC and fan noise
* **`traffic`**: Vehicle and road noise
* **`crowd`**: Background conversations and crowd noise
## Job Management
### Get Job Status
Monitor the progress of noise reduction jobs.
```http theme={null}
GET /api/v1/denoiser/jobs/{job_id}
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
f"https://api.audiopod.ai/api/v1/denoiser/jobs/{job_id}",
headers={"X-API-Key": api_key}
)
if response.status_code == 200:
job_status = response.json()
print(f"Status: {job_status['status']}")
print(f"Progress: {job_status['progress']}%")
if job_status["status"] == "COMPLETED":
print("Denoising complete!")
print(f"Output URL: {job_status['output_url']}")
```
**Response (Completed):**
```json theme={null}
{
"id": 123,
"status": "COMPLETED",
"progress": 100,
"input_path": "/uploads/noisy_recording.wav",
"output_path": "/processed/clean_recording_123.wav",
"video_output_path": null,
"is_video": false,
"task_id": "celery_task_uuid_here",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:32:15Z",
"completed_at": "2024-01-15T10:32:15Z",
"output_url": "https://api.audiopod.ai/download/clean_recording_123.wav",
"video_output_url": null,
"original_filename": "noisy_recording.wav",
"display_name": "Clean Recording",
"stats": {
"noise_reduction_db": 15.2,
"processing_time": 135.4,
"audio_duration": 180.0
}
}
```
### List Denoising Jobs
Get all noise reduction jobs for the authenticated user.
```http theme={null}
GET /api/v1/denoiser/jobs?limit=50&skip=0
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
"https://api.audiopod.ai/api/v1/denoiser/jobs",
headers={"X-API-Key": api_key},
params={
"limit": 50,
"skip": 0
}
)
if response.status_code == 200:
jobs = response.json()
for job in jobs:
print(f"Job {job['id']}: {job['status']} - {job['original_filename']}")
```
### Retry Failed Job
Retry a failed noise reduction job.
```http theme={null}
POST /api/v1/denoiser/jobs/{job_id}/retry
X-API-Key: {api_key}
```
### Cancel Job
Cancel a pending denoising job.
```http theme={null}
POST /api/v1/denoiser/jobs/{job_id}/cancel
X-API-Key: {api_key}
```
```python theme={null}
response = requests.post(
f"https://api.audiopod.ai/api/v1/denoiser/jobs/{job_id}/cancel",
headers={"X-API-Key": api_key}
)
if response.status_code == 200:
job_status = response.json()
print(f"Job {job_id} cancelled: {job_status['status']}")
```
### Delete Job
Remove a completed job and its results.
```http theme={null}
DELETE /api/v1/denoiser/jobs/{job_id}
X-API-Key: {api_key}
```
```python theme={null}
response = requests.delete(
f"https://api.audiopod.ai/api/v1/denoiser/jobs/{job_id}",
headers={"X-API-Key": api_key}
)
if response.status_code == 204:
print(f"Job {job_id} and associated files deleted successfully")
```
### Health Check
Check the denoising service availability and status.
```http theme={null}
GET /api/v1/denoiser/health
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
"https://api.audiopod.ai/api/v1/denoiser/health",
headers={"X-API-Key": api_key}
)
if response.status_code == 200:
health_status = response.json()
print(f"Service status: {health_status['status']}")
```
**Health Response:**
```json theme={null}
{
"status": "available"
}
```
## Error Handling
**Causes:** - Audio file corrupted or invalid format - File contains no audio content - Unsupported codec
**Solutions:** - Use supported formats (WAV, MP3, M4A, MP4) - Verify file integrity - Check audio content exists
**Causes:** - The uploaded file is larger than your plan allows per upload
**Solutions:** - Upgrade your plan for larger uploads - Trim or compress the file before uploading
**Causes:** - `quality_mode` is not one of `balanced`, `studio` or `ultra` - Audio quality too poor for processing - Extreme noise levels - Very short audio duration
**Solutions:** - Try different quality mode - Use spectral processing for specific noise types - Ensure minimum 5 seconds duration
**Causes:** - Not enough credits for processing duration
**Solutions:** - Purchase additional credits - Check credit requirements before processing
## Pricing
Signed-in accounts are billed from the **account credit balance** at the rate
of the chosen quality mode, with a 10-credit minimum per job. API-key calls pay
the API price of the chosen mode. Call `POST /api/v1/denoiser/quote` to see the
exact cost before you start a job.
| Service | Quality mode | Cost |
| - | - | - |
| AI Denoising | `balanced` | 330 credits/minute |
| AI Denoising | `studio` (default) | 660 credits/minute |
| AI Denoising | `ultra` | 1,320 credits/minute |
| Spectral Processing | — | 660 credits/minute |
### Cost Examples
| Duration | Method | Credits |
| - | - | - |
| 10 minutes | AI Denoising (balanced) | 3,300 |
| 10 minutes | AI Denoising (studio) | 6,600 |
| 10 minutes | AI Denoising (ultra) | 13,200 |
| 30 minutes | Spectral | 19,800 |
| 1 hour | AI Denoising (studio) | 39,600 |
## Next Steps
Use cleaned audio for better voice model training.
Improve transcription accuracy with clean audio.
# Podcast
Source: https://docs.audiopod.ai/api-reference/podcast
Generate multi-speaker podcast episodes from your source material — outline, transcript, and narrated audio.
## Overview
The Podcast API turns your topic and source material into a finished,
multi-speaker episode. A project moves through a simple lifecycle: create the
project and its speakers, add sources, generate an outline, generate the
transcript, then render the narrated audio. The RSS feed is limited while it
is rebuilt; see [RSS feed](#rss-feed-limited) below.
## Authentication
* **API Key (Recommended)**: `X-API-Key: your_api_key` header
* **JWT Token**: `Authorization: Bearer your_jwt_token`
## Lifecycle at a glance
| Step | Endpoint |
| - | - |
| 1. Create a project (+ speakers) | `POST /api/v1/podcast/projects` |
| 2. Add source material | `POST /api/v1/podcast/projects/{id}/sources` |
| 3. Generate the outline | `POST /api/v1/podcast/projects/{id}/outline/generate` |
| 4. Generate the transcript | `POST /api/v1/podcast/projects/{id}/transcript/generate` |
| 5. Estimate + render audio | `GET /api/v1/podcast/projects/{id}/audio/estimate` · `POST /api/v1/podcast/projects/{id}/audio/generate` |
| 6. Track progress | `GET /api/v1/podcast/projects/{id}/progress` |
| 7. RSS feed (limited) | `POST /api/v1/podcast/feed` |
## Create a Project
```http theme={null}
POST /api/v1/podcast/projects
X-API-Key: {api_key}
Content-Type: application/json
{
"title": "The Future of Audio AI",
"topic": "How on-device audio models change creator workflows",
"briefing": "Keep it upbeat and accessible for a general audience.",
"language": "en",
"target_duration_minutes": 15,
"speakers": [
{ "name": "Host", "voice": "aura" },
{ "name": "Guest", "voice": "sage" }
]
}
```
```python theme={null}
import requests
project = requests.post(
"https://api.audiopod.ai/api/v1/podcast/projects",
headers={"X-API-Key": api_key, "Content-Type": "application/json"},
json={
"title": "The Future of Audio AI",
"topic": "How on-device audio models change creator workflows",
"language": "en",
"target_duration_minutes": 15,
"speakers": [
{"name": "Host", "voice": "aura"},
{"name": "Guest", "voice": "sage"},
],
},
).json()
print(f"Project {project['id']} created")
```
**Key fields:**
* `title` (required): Episode title (1–200 characters)
* `topic` (required): Main topic (1–500 characters)
* `briefing` (optional): Extra context / tone guidance
* `key_points` (optional): List of points to cover
* `language` (optional): Primary language code (default `en`)
* `target_duration_minutes` (optional): Target length, 5–60
* `speakers` (required): 1–4 initial speakers
## Manage Projects
| Action | Request |
| - | - |
| List projects | `GET /api/v1/podcast/projects` |
| Get one | `GET /api/v1/podcast/projects/{id}` |
| Update | `PUT /api/v1/podcast/projects/{id}` |
| Delete | `DELETE /api/v1/podcast/projects/{id}` |
## Update a Speaker
Change a speaker's name or assigned voice on a project.
```http theme={null}
PUT /api/v1/podcast/projects/{project_id}/speakers/{speaker_id}
X-API-Key: {api_key}
Content-Type: application/json
{
"name": "Co-Host",
"voice": "willow"
}
```
## Generate Audio
Preview the cost, then render. Audio generation follows the prepaid model —
credits are reserved up front and refunded automatically on failure.
```bash theme={null}
# Estimate
curl -s "https://api.audiopod.ai/api/v1/podcast/projects/$PROJECT_ID/audio/estimate" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
# Render
curl -X POST "https://api.audiopod.ai/api/v1/podcast/projects/$PROJECT_ID/audio/generate" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
Poll `GET /api/v1/podcast/projects/{id}/progress` until generation completes.
## RSS Feed (limited)
The RSS feed is being rebuilt and is limited until then. Today it lists every
completed episode on your account, and anyone with the feed URL can read it.
Listing is moving to per-episode opt-in. The feed has no show artwork yet, so
podcast directories such as Apple Podcasts and Spotify will not accept it. To
publish an episode today, download its audio and upload it to your podcast
host.
The feed URL carries a signed, feed-scoped token.
```http theme={null}
POST /api/v1/podcast/feed
X-API-Key: {api_key}
```
```json theme={null}
{
"feed_url": "https://api.audiopod.ai/api/v1/podcast/feed/.xml"
}
```
The feed itself is fetched anonymously by podcast players:
```http theme={null}
GET /api/v1/podcast/feed/{token}.xml
```
## Next Steps
Narrate a single article or block of text.
Browse voices or design a custom one for your hosts.
# Audio Reader
Source: https://docs.audiopod.ai/api-reference/reader
Turn any article, blog post, or pasted text into natural-sounding narrated audio — from a URL or raw content — and share it with a public link.
## Overview
The Audio Reader converts a web article or pasted text into a narrated audio
track. Give it a URL (AudioPod extracts the readable article) or raw `content`,
pick a voice, and get back a hosted audio file plus an optional public
share/embed page.
### Key features
* **URL or pasted text**: Point at an article URL or send `content` directly
* **Any voice**: Free standard narration, or a premium/custom voice from your catalog
* **Cost preview**: Estimate credits before you commit
* **Public share + embed**: Each job can expose a minimal public page and embed HTML
## Authentication
* **API Key (Recommended)**: `X-API-Key: your_api_key` header
* **JWT Token**: `Authorization: Bearer your_jwt_token`
## Estimate Cost
Preview the cost before generating. No charge is incurred for an estimate.
```http theme={null}
POST /api/v1/reader/estimate
X-API-Key: {api_key}
Content-Type: application/json
{
"url": "https://example.com/blog/my-article",
"voice": "aura"
}
```
**Body:**
* `url` (optional): Article URL to read (provide `url` **or** `content`)
* `content` (optional): Raw text to narrate
* `voice` (optional): Voice name or ID from `GET /api/v1/voice/voice-profiles`
**Response:**
```json theme={null}
{
"is_premium": true,
"estimated_credits": 1840,
"word_count": 920,
"char_count": 5230,
"measured": true,
"title": "My Article",
"note": null
}
```
When `is_premium` is `false`, standard narration is used at no credit cost. When
`measured` is `false`, the article couldn't be scraped ahead of time, so
`estimated_credits` is a baseline and you're billed for the actual length.
## Generate
Create a reader job. Billing follows the standard prepaid model — credits are
reserved on creation and refunded automatically if processing fails.
```http theme={null}
POST /api/v1/reader/generate
X-API-Key: {api_key}
Content-Type: application/json
{
"url": "https://example.com/blog/my-article",
"voice": "aura",
"show_branding": true
}
```
```python theme={null}
import requests
job = requests.post(
"https://api.audiopod.ai/api/v1/reader/generate",
headers={"X-API-Key": api_key, "Content-Type": "application/json"},
json={"url": "https://example.com/blog/my-article", "voice": "aura"},
).json()
print(f"Reader job {job['id']} — {job['status']}")
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/reader/generate" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/blog/my-article", "voice": "aura"}'
```
**Body:**
* `url` (optional): Article URL to read (provide `url` **or** `content`)
* `content` (optional): Raw text to narrate
* `title` (optional): Override the detected title
* `voice` (optional): Voice name or ID; defaults to a standard narration voice
* `webhook_url` (optional): Receive a callback when the job finishes
* `show_branding` (optional): Show AudioPod branding on the public page (default `true`)
**Response:**
```json theme={null}
{
"id": 4210,
"status": "processing",
"progress": 0,
"title": "My Article",
"word_count": 920,
"audio_url": null,
"embed_url": null,
"embed_html": null,
"audio_duration_seconds": null,
"created_at": "2026-07-09T10:30:00Z",
"completed_at": null
}
```
## Get a Job
```http theme={null}
GET /api/v1/reader/{job_id}
X-API-Key: {api_key}
```
Poll until `status` is `completed`; the response then carries `audio_url`,
`audio_duration_seconds`, and `embed_url` / `embed_html` for embedding.
## List Jobs
```http theme={null}
GET /api/v1/reader/
X-API-Key: {api_key}
```
Returns a paginated list of your reader jobs.
## Public Share Page
Fetch the minimal, unauthenticated view for a shared job — safe to call without
a key. It exposes only what the share link already implies (title, audio URL,
duration), never the source URL, content, or any internals.
```http theme={null}
GET /api/v1/reader/public/{job_id}
```
**Response:**
```json theme={null}
{
"id": 4210,
"status": "completed",
"title": "My Article",
"audio_url": "https://media.audiopod.ai/...",
"audio_duration_seconds": 372,
"show_branding": true
}
```
## Delete a Job
```http theme={null}
DELETE /api/v1/reader/{job_id}
X-API-Key: {api_key}
```
## Next Steps
Fine-grained control over voice, directing, and timestamps.
Turn source material into a multi-speaker episode.
# Speaker Extraction
Source: https://docs.audiopod.ai/api-reference/speaker-separation
Extract individual speakers from multi-speaker audio recordings into separate, clean audio files using advanced AI-powered speaker separation technology.
## Overview
AudioPod AI's Speaker Extraction API automatically separates multiple speakers in audio recordings into individual speaker-specific audio files. The service identifies who speaks when and creates clean, separate audio tracks for each speaker while preserving original audio quality.
### Key Features
* **Speaker Separation**: Generate separate audio files for each detected speaker
* **Timeline Generation**: Get detailed RTTM files with speaker timestamps
* **Speaker Analytics**: Duration and quality statistics for each speaker
* **Multi-Format Support**: Process audio and video files (WAV, MP3, M4A, MP4, etc.)
* **URL Processing**: Extract speakers from YouTube and other video platforms
* **Smart Detection**: Automatic speaker detection or specify expected number
* **Quality Preservation**: Maintains original audio quality in extracted files
## Authentication
All endpoints require authentication. Use one of these methods:
* **API Key (Recommended)**: `X-API-Key: your_api_key` header
* **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth)
## Speaker Extraction
### Extract from File Upload
Upload an audio or video file to extract individual speaker tracks.
```http theme={null}
POST /api/v1/speaker/extract
X-API-Key: {api_key}
Content-Type: multipart/form-data
file: (audio/video file)
num_speakers: 4
```
```python theme={null}
import requests
with open("podcast_episode.mp3", "rb") as audio_file:
response = requests.post(
"https://api.audiopod.ai/api/v1/speaker/extract",
headers={"X-API-Key": api_key},
data={"num_speakers": 4}, # Optional: specify expected speakers
files={"file": audio_file}
)
if response.status_code == 200:
extraction_job = response.json()
job_id = extraction_job["id"]
print(f"Speaker extraction job created: {job_id}")
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/speaker/extract" \
-H "X-API-Key: your_api_key" \
-F "file=@podcast_episode.mp3" \
-F "num_speakers=4"
```
### Extract from URL
Extract speakers from audio/video URLs (YouTube, Vimeo, etc.).
```http theme={null}
POST /api/v1/speaker/extract
X-API-Key: {api_key}
Content-Type: application/x-www-form-urlencoded
url=https://youtube.com/watch?v=example123&num_speakers=3
```
```python theme={null}
response = requests.post(
"https://api.audiopod.ai/api/v1/speaker/extract",
headers={"X-API-Key": api_key},
data={
"url": "https://youtube.com/watch?v=example123",
"num_speakers": 3 # Optional: specify expected speakers
}
)
if response.status_code == 200:
job_data = response.json()
print(f"URL extraction started: {job_data['id']}")
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/speaker/extract" \
-H "X-API-Key: your_api_key" \
-d "url=https://youtube.com/watch?v=example123" \
-d "num_speakers=3"
```
**Response:**
```json theme={null}
{
"id": 123,
"job_type": "extraction",
"status": "PENDING",
"created_at": "2024-01-15T10:30:00Z",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"task_id": "celery_task_uuid_here"
}
```
## Job Management
### Get Job Status
Monitor the progress of speaker extraction jobs.
```http theme={null}
GET /api/v1/speaker/jobs/{job_id}
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
f"https://api.audiopod.ai/api/v1/speaker/jobs/{job_id}",
headers={"X-API-Key": api_key}
)
if response.status_code == 200:
job_status = response.json()
print(f"Status: {job_status['status']}")
if job_status["status"] == "COMPLETED":
print("Extraction complete!")
if job_status["result"]:
result = job_status["result"]
print(f"Extracted {len(result['speakers'])} speakers")
for speaker in result['speakers']:
print(f"- {speaker['label']}: {speaker.get('download_url', 'Processing...')}")
```
**Response (Completed Extraction):**
```json theme={null}
{
"id": 123,
"job_type": "extraction",
"status": "COMPLETED",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:35:30Z",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"task_id": "celery_task_uuid_here",
"result": {
"speakers": [
{
"id": 0,
"label": "SPEAKER_0",
"audio_path": "processed/123/speaker_0.wav",
"download_url": "https://s3.amazonaws.com/...",
"audio_stats": {
"rms_db": -12.3,
"peak": 0.85
}
},
{
"id": 1,
"label": "SPEAKER_1",
"audio_path": "processed/123/speaker_1.wav",
"download_url": "https://s3.amazonaws.com/...",
"audio_stats": {
"rms_db": -15.7,
"peak": 0.72
}
}
],
"files": [
{
"type": "audio",
"speaker": "SPEAKER_0",
"path": "processed/123/speaker_0.wav",
"download_url": "https://s3.amazonaws.com/..."
},
{
"type": "audio",
"speaker": "SPEAKER_1",
"path": "processed/123/speaker_1.wav",
"download_url": "https://s3.amazonaws.com/..."
},
{
"type": "rttm",
"path": "processed/123/extraction.rttm",
"download_url": "https://s3.amazonaws.com/..."
}
],
"rttm_path": "processed/123/extraction.rttm"
}
}
```
### List Extraction Jobs
Get all speaker extraction jobs for the authenticated user.
```http theme={null}
GET /api/v1/speaker/jobs?job_type=extraction&status=COMPLETED&limit=50
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
"https://api.audiopod.ai/api/v1/speaker/jobs",
headers={"X-API-Key": api_key},
params={
"job_type": "extraction",
"status": "COMPLETED", # Optional filter
"skip": 0,
"limit": 50
}
)
if response.status_code == 200:
jobs_data = response.json()
print(f"Total jobs: {jobs_data['total']}")
print(f"Has more: {jobs_data['hasMore']}")
for job in jobs_data["items"]:
print(f"Job {job['id']}: {job['status']} - {job.get('filename', 'N/A')}")
if job['status'] == 'COMPLETED' and job.get('outputFiles'):
print(f" Output files: {len(job['outputFiles'])}")
```
**Response:**
```json theme={null}
{
"items": [
{
"id": 123,
"job_type": "extraction",
"status": "COMPLETED",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:35:30Z",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"task_id": "celery_task_uuid_here",
"filename": "podcast_episode.mp3",
"display_name": "podcast_episode.mp3",
"outputFiles": [
{
"type": "audio",
"speaker": "SPEAKER_0",
"path": "processed/123/speaker_0.wav"
},
{
"type": "audio",
"speaker": "SPEAKER_1",
"path": "processed/123/speaker_1.wav"
}
]
}
],
"hasMore": false,
"total": 1
}
```
### Retry Failed Job
Retry a failed speaker extraction job.
```http theme={null}
POST /api/v1/speaker/jobs/{job_id}/retry
X-API-Key: {api_key}
```
```python theme={null}
response = requests.post(
f"https://api.audiopod.ai/api/v1/speaker/jobs/{job_id}/retry",
headers={"X-API-Key": api_key}
)
if response.status_code == 200:
retried_job = response.json()
print(f"Extraction job {retried_job['id']} retried successfully")
print(f"New task ID: {retried_job['task_id']}")
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/speaker/jobs/123/retry" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Response:**
```json theme={null}
{
"id": 123,
"job_type": "extraction",
"status": "PROCESSING",
"created_at": "2024-01-15T10:30:00Z",
"task_id": "new_celery_task_uuid_here",
"user_id": "550e8400-e29b-41d4-a716-446655440000"
}
```
### Delete Job
Remove a speaker extraction job and its associated files.
```http theme={null}
DELETE /api/v1/speaker/jobs/{job_id}
X-API-Key: {api_key}
```
```python theme={null}
response = requests.delete(
f"https://api.audiopod.ai/api/v1/speaker/jobs/{job_id}",
headers={"X-API-Key": api_key}
)
if response.status_code == 204:
print("Extraction job and files deleted successfully")
elif response.status_code == 404:
print("Job not found or access denied")
```
```bash theme={null}
curl -X DELETE "https://api.audiopod.ai/api/v1/speaker/jobs/123" \
-H "X-API-Key: your_api_key"
```
**Response:** `204 No Content` on successful deletion
## Supported Formats
**Audio Formats:**
* WAV, MP3, M4A, AAC, FLAC, OGG, OPUS, WebM
* WMA, Speex, and other common formats
**Video Formats:**
* MP4, AVI, MOV, MKV, WebM
* Audio will be extracted automatically from video files
**URL Sources:**
* YouTube, Vimeo, and other video platforms
* Direct audio/video file URLs
## Error Handling
```json theme={null}
{
"error_code": "INVALID_AUDIO_FORMAT",
"message": "Invalid file type. Must be audio or video file.",
"details": {
"content_type": "text/plain",
"extension": ".txt",
"supported_formats": ["audio/wav", "audio/mp3", "audio/m4a", "video/mp4"],
"supported_extensions": [".wav", ".mp3", ".m4a", ".mp4", ".avi", ".mov"]
}
}
```
**Causes:** Invalid file format, missing file/URL, or both file and URL provided
**Solutions:** Use supported audio/video formats, provide either file OR URL (not both)
```json theme={null}
{
"detail": "Insufficient credits for processing. Required: 8250, Available: 1000"
}
```
**Causes:** Not enough credits for the audio duration
**Solutions:** Purchase additional credits or process shorter audio files
```json theme={null}
{
"error_code": "PROCESSING_FAILED",
"message": "Failed to extract speakers from audio",
"details": {
"reason": "Audio quality too poor or no distinguishable speakers found"
}
}
```
**Causes:** Poor audio quality, no speech content, or indistinguishable speakers
**Solutions:** Ensure clear speech content, try noise reduction first, or verify multiple speakers exist
```json theme={null}
{
"detail": "Job not found or access denied"
}
```
**Causes:** Invalid job ID or trying to access another user's job
**Solutions:** Verify job ID and ensure you own the job
```json theme={null}
{
"detail": "Rate limit exceeded. Try again later."
}
```
**Causes:** Exceeded 100 requests per minute limit
**Solutions:** Wait before making additional requests or implement request throttling
## Pricing
Speaker extraction costs are based on audio duration:
| Service | Cost | Description |
| - | - | - |
| Speaker Extraction | 330 credits/minute | Generate separate audio files for each speaker |
*Note: Credits are charged per second of audio (5.5 credits/second)*
### Cost Examples
| Duration | Service | Credits | USD Cost\* |
| - | - | - | - |
| 5 minutes | Extraction | 1,650 | \~\$0.22 |
| 15 minutes | Extraction | 4,950 | \~\$0.66 |
| 30 minutes | Extraction | 9,900 | \~\$1.32 |
| 1 hour | Extraction | 19,800 | \~\$2.64 |
\*USD cost estimates based on standard credit pricing. Actual costs may vary based on subscription plan.
## Rate Limits
* **100 requests per minute** per API key
* Rate limits apply per endpoint
* Exceeding limits returns `429 Too Many Requests`
## Next Steps
Transcribe individual speaker tracks with improved accuracy.
Clean up audio before speaker extraction for better results.
# Speech-to-Text
Source: https://docs.audiopod.ai/api-reference/speech-to-text
Convert audio and video to accurate text transcriptions with speaker identification, timestamps, and multi-language support.
## Overview
AudioPod AI's Speech-to-Text API converts audio and video content into accurate text transcriptions using AudioPod's transcription engine. Get detailed transcriptions with speaker diarization, word-level timestamps, and confidence scores.
### Key Features
* **Multiple accuracy / speed tiers**: choose the engine variant that matches your latency and quality needs
* **Speaker Diarization**: Automatic speaker identification and separation
* **Word-Level Timestamps**: Precise timing for each word
* **Confidence Scores**: Quality metrics for transcription accuracy
* **50+ Languages**: Automatic language detection or manual specification
* **Large File Support**: Handle videos up to 15 hours with chunking
* **Multiple Sources**: Upload files or provide YouTube/video URLs
* **Editable Transcripts**: Edit and refine transcription results
## Authentication
All endpoints require authentication. Use one of these methods:
* **API Key (Recommended)**: `X-API-Key: your_api_key` header
* **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth)
## Transcribe from URLs
### Transcribe YouTube Videos
Transcribe audio from YouTube or other video platforms.
```python theme={null}
from audiopod import Client
client = Client()
# Simple YouTube transcription
transcription = client.transcription.transcribe_from_url(
url="https://youtube.com/watch?v=example123",
wait_for_completion=True # Wait for result
)
print(f"Transcription completed!")
print(f"Duration: {transcription.duration}s")
print(f"Language: {transcription.language}")
print(f"Full text: {transcription.text}")
# Advanced transcription with speaker diarization
advanced_transcription = client.transcription.transcribe_from_url(
url="https://youtube.com/watch?v=example123",
language="en", # Optional: auto-detect if not specified
model_type="premium", # "premium" / "premium-diarized"; omit for Standard accuracy
enable_speaker_diarization=True,
min_speakers=2,
max_speakers=5,
enable_word_timestamps=True,
enable_confidence_scores=True,
chunk_duration=1800, # 30 minutes per chunk
wait_for_completion=True
)
# Access speaker-separated text
for segment in advanced_transcription.segments:
speaker = segment.get('speaker', 'Unknown')
text = segment['text']
start_time = segment['start']
end_time = segment['end']
confidence = segment.get('confidence', 0.0)
print(f"[{start_time:.2f}s - {end_time:.2f}s] {speaker}: {text} (confidence: {confidence:.2f})")
# Batch processing multiple URLs
urls = [
"https://youtube.com/watch?v=video1",
"https://youtube.com/watch?v=video2",
"https://vimeo.com/123456789"
]
batch_results = client.transcription.transcribe_batch_from_urls(
urls=urls,
enable_speaker_diarization=True,
model_type="premium",
wait_for_completion=True
)
for i, result in enumerate(batch_results):
print(f"\nVideo {i+1}: {urls[i]}")
print(f"Status: {result.status}")
if result.status == "completed":
print(f"Text preview: {result.text[:100]}...")
```
```javascript theme={null}
const { AudioPodClient } = require('audiopod-js');
const client = new AudioPodClient();
async function transcribeYouTubeVideo() {
try {
// Simple transcription
const transcription = await client.transcription.transcribeFromUrl({
url: "https://youtube.com/watch?v=example123",
waitForCompletion: true
});
console.log(`Transcription completed!`);
console.log(`Duration: ${transcription.duration}s`);
console.log(`Language: ${transcription.language}`);
console.log(`Full text: ${transcription.text}`);
// Advanced transcription with speaker diarization
const advancedTranscription = await client.transcription.transcribeFromUrl({
url: "https://youtube.com/watch?v=example123",
language: "en",
modelType: "premium",
enableSpeakerDiarization: true,
minSpeakers: 2,
maxSpeakers: 5,
enableWordTimestamps: true,
enableConfidenceScores: true,
chunkDuration: 1800,
waitForCompletion: true
});
// Process speaker-separated segments
advancedTranscription.segments.forEach(segment => {
const speaker = segment.speaker || 'Unknown';
const text = segment.text;
const startTime = segment.start;
const endTime = segment.end;
const confidence = segment.confidence || 0.0;
console.log(`[${startTime.toFixed(2)}s - ${endTime.toFixed(2)}s] ${speaker}: ${text} (confidence: ${confidence.toFixed(2)})`);
});
// Batch processing
const urls = [
"https://youtube.com/watch?v=video1",
"https://youtube.com/watch?v=video2",
"https://vimeo.com/123456789"
];
const batchResults = await client.transcription.transcribeBatchFromUrls({
urls: urls,
enableSpeakerDiarization: true,
modelType: "premium",
waitForCompletion: true
});
batchResults.forEach((result, index) => {
console.log(`\nVideo ${index + 1}: ${urls[index]}`);
console.log(`Status: ${result.status}`);
if (result.status === "completed") {
console.log(`Text preview: ${result.text.substring(0, 100)}...`);
}
});
} catch (error) {
console.error('Transcription error:', error.message);
}
}
transcribeYouTubeVideo();
```
```python theme={null}
import requests
import time
# Start transcription job
response = requests.post(
"https://api.audiopod.ai/api/v1/transcription/transcribe",
headers={"X-API-Key": api_key},
json={
"source_urls": [
"https://youtube.com/watch?v=example123",
"https://vimeo.com/123456789"
],
"language": "en", # Optional: auto-detect if not specified
"model_type": "premium",
"enable_speaker_diarization": True,
"min_speakers": 2,
"max_speakers": 5,
"enable_word_timestamps": True,
"enable_confidence_scores": True,
"chunk_duration": 1800 # 30 minutes
}
)
if response.status_code == 200:
job_data = response.json()
job_id = job_data["job_id"]
print(f"Transcription job created: {job_id}")
# Poll for completion
while True:
status_response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/status/{job_id}",
headers={"X-API-Key": api_key}
)
if status_response.status_code == 200:
status_data = status_response.json()
print(f"Status: {status_data['status']}")
if status_data['status'] == 'completed':
# Get final result
result_response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/result/{job_id}",
headers={"X-API-Key": api_key}
)
if result_response.status_code == 200:
result = result_response.json()
print(f"Transcription text: {result['transcription']}")
# Print speaker segments if diarization was enabled
if 'segments' in result:
for segment in result['segments']:
speaker = segment.get('speaker', 'Unknown')
text = segment['text']
start = segment['start']
end = segment['end']
print(f"[{start:.2f}s - {end:.2f}s] {speaker}: {text}")
break
elif status_data['status'] == 'failed':
print(f"Transcription failed: {status_data.get('error', 'Unknown error')}")
break
time.sleep(10) # Wait 10 seconds before checking again
```
```bash theme={null}
# Start transcription job
curl -X POST "https://api.audiopod.ai/api/v1/transcription/transcribe" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_urls": ["https://youtube.com/watch?v=example123"],
"language": "en",
"model_type": "premium",
"enable_speaker_diarization": true,
"enable_word_timestamps": true,
"enable_confidence_scores": true
}'
# Check job status (replace JOB_ID with actual job ID)
curl -X GET "https://api.audiopod.ai/api/v1/transcription/status/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
# Get transcription result when completed
curl -X GET "https://api.audiopod.ai/api/v1/transcription/result/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
**Response:**
```json theme={null}
{
"job_id": 123,
"task_id": "celery_task_uuid_here",
"status": "PENDING",
"message": "Transcription job created successfully",
"estimated_credits": 150,
"estimated_duration": 1800.0,
"source_urls": [
"https://youtube.com/watch?v=example123"
]
}
```
## Transcribe from Files
### Upload Audio/Video Files
Transcribe from uploaded audio or video files.
```python theme={null}
from audiopod import Client
import os
client = Client()
# Single file transcription
transcription = client.transcription.transcribe_from_file(
audio_file="meeting_recording.mp3",
language="en",
model_type="premium",
enable_speaker_diarization=True,
min_speakers=2,
max_speakers=8,
enable_word_timestamps=True,
enable_confidence_scores=True,
wait_for_completion=True
)
print(f"Transcription completed!")
print(f"File: {transcription.source_file}")
print(f"Duration: {transcription.duration}s")
print(f"Language: {transcription.language}")
print(f"Full text: {transcription.text}")
# Access detailed segments with speakers
for segment in transcription.segments:
speaker = segment.get('speaker', 'Unknown')
text = segment['text']
start_time = segment['start']
end_time = segment['end']
confidence = segment.get('confidence', 0.0)
print(f"[{start_time:.2f}s - {end_time:.2f}s] {speaker}: {text}")
# Batch file transcription
audio_files = [
"meeting_recording.mp3",
"interview.wav",
"presentation.mp4",
"podcast_episode.m4a"
]
batch_results = client.transcription.transcribe_batch_from_files(
audio_files=audio_files,
enable_speaker_diarization=True,
model_type="premium",
enable_word_timestamps=True,
chunk_duration=1800, # 30 minutes per chunk
wait_for_completion=True
)
# Process results
for i, result in enumerate(batch_results):
print(f"\nFile {i+1}: {audio_files[i]}")
print(f"Status: {result.status}")
if result.status == "completed":
print(f"Duration: {result.duration}s")
print(f"Language: {result.language}")
print(f"Text preview: {result.text[:100]}...")
# Save transcription to file
output_file = f"transcript_{i+1}.txt"
with open(output_file, 'w', encoding='utf-8') as f:
f.write(result.text)
print(f"Saved to: {output_file}")
# Advanced file processing with custom settings
def process_interview_file(file_path):
"""Process interview with optimized settings"""
transcription = client.transcription.transcribe_from_file(
audio_file=file_path,
model_type="premium", # Premium accuracy
enable_speaker_diarization=True,
min_speakers=2, # Interviewer + interviewee
max_speakers=4, # Allow for additional participants
enable_word_timestamps=True,
enable_confidence_scores=True,
language="auto", # Auto-detect language
wait_for_completion=True
)
# Generate formatted transcript
formatted_output = []
current_speaker = None
for segment in transcription.segments:
speaker = segment.get('speaker', 'Unknown')
text = segment['text'].strip()
if speaker != current_speaker:
formatted_output.append(f"\n{speaker}:")
current_speaker = speaker
formatted_output.append(f" {text}")
return ''.join(formatted_output)
# Process interview
interview_transcript = process_interview_file("important_interview.wav")
print("\nFormatted Interview Transcript:")
print(interview_transcript)
```
```javascript theme={null}
const { AudioPodClient } = require('audiopod-js');
const fs = require('fs');
const path = require('path');
const client = new AudioPodClient();
async function transcribeAudioFiles() {
try {
// Single file transcription
const transcription = await client.transcription.transcribeFromFile({
audioFile: fs.createReadStream('meeting_recording.mp3'),
language: "en",
modelType: "premium",
enableSpeakerDiarization: true,
minSpeakers: 2,
maxSpeakers: 8,
enableWordTimestamps: true,
enableConfidenceScores: true,
waitForCompletion: true
});
console.log(`Transcription completed!`);
console.log(`File: ${transcription.sourceFile}`);
console.log(`Duration: ${transcription.duration}s`);
console.log(`Language: ${transcription.language}`);
console.log(`Full text: ${transcription.text}`);
// Process segments with speakers
transcription.segments.forEach(segment => {
const speaker = segment.speaker || 'Unknown';
const text = segment.text;
const startTime = segment.start;
const endTime = segment.end;
const confidence = segment.confidence || 0.0;
console.log(`[${startTime.toFixed(2)}s - ${endTime.toFixed(2)}s] ${speaker}: ${text}`);
});
// Batch processing
const audioFiles = [
'meeting_recording.mp3',
'interview.wav',
'presentation.mp4',
'podcast_episode.m4a'
];
const fileStreams = audioFiles.map(file => ({
file: fs.createReadStream(file),
name: path.basename(file)
}));
const batchResults = await client.transcription.transcribeBatchFromFiles({
audioFiles: fileStreams,
enableSpeakerDiarization: true,
modelType: "premium",
enableWordTimestamps: true,
chunkDuration: 1800,
waitForCompletion: true
});
// Process batch results
batchResults.forEach((result, index) => {
console.log(`\nFile ${index + 1}: ${audioFiles[index]}`);
console.log(`Status: ${result.status}`);
if (result.status === "completed") {
console.log(`Duration: ${result.duration}s`);
console.log(`Language: ${result.language}`);
console.log(`Text preview: ${result.text.substring(0, 100)}...`);
// Save transcription to file
const outputFile = `transcript_${index + 1}.txt`;
fs.writeFileSync(outputFile, result.text, 'utf8');
console.log(`Saved to: ${outputFile}`);
}
});
// Advanced processing function for interviews
async function processInterviewFile(filePath) {
const transcription = await client.transcription.transcribeFromFile({
audioFile: fs.createReadStream(filePath),
modelType: "premium",
enableSpeakerDiarization: true,
minSpeakers: 2,
maxSpeakers: 4,
enableWordTimestamps: true,
enableConfidenceScores: true,
language: "auto",
waitForCompletion: true
});
// Format transcript with speaker labels
const formattedOutput = [];
let currentSpeaker = null;
transcription.segments.forEach(segment => {
const speaker = segment.speaker || 'Unknown';
const text = segment.text.trim();
if (speaker !== currentSpeaker) {
formattedOutput.push(`\n${speaker}:`);
currentSpeaker = speaker;
}
formattedOutput.push(` ${text}`);
});
return formattedOutput.join('');
}
// Process an interview
const interviewTranscript = await processInterviewFile('important_interview.wav');
console.log('\nFormatted Interview Transcript:');
console.log(interviewTranscript);
} catch (error) {
console.error('Transcription error:', error.message);
}
}
transcribeAudioFiles();
```
```python theme={null}
import requests
import os
# Prepare files for upload
files_to_transcribe = [
"meeting_recording.mp3",
"interview.wav",
"presentation.mp4"
]
files = []
for file_path in files_to_transcribe:
if os.path.exists(file_path):
files.append(('files', open(file_path, 'rb')))
# Upload and transcribe
response = requests.post(
"https://api.audiopod.ai/api/v1/transcription/transcribe-upload",
headers={"X-API-Key": api_key},
data={
"language": "en",
"model_type": "premium",
"enable_speaker_diarization": True,
"min_speakers": 1,
"max_speakers": 10,
"enable_word_timestamps": True,
"enable_confidence_scores": True,
"chunk_duration": 1800
},
files=files
)
# Close file handles
for _, file_obj in files:
file_obj.close()
if response.status_code == 200:
job_data = response.json()
job_id = job_data['job_id']
print(f"Upload transcription job: {job_id}")
# Poll for completion
import time
while True:
status_response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}",
headers={"X-API-Key": api_key}
)
if status_response.status_code == 200:
status_data = status_response.json()
print(f"Status: {status_data['status']}")
if status_data['status'] == 'COMPLETED':
# Get transcription result
result_response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/transcript/{job_id}",
headers={"X-API-Key": api_key}
)
if result_response.status_code == 200:
transcript_data = result_response.json()
print(f"Transcription completed!")
print(f"Full text: {transcript_data['transcript']}")
# Save to file
with open(f"transcript_{job_id}.txt", 'w', encoding='utf-8') as f:
f.write(transcript_data['transcript'])
print(f"Saved to transcript_{job_id}.txt")
break
elif status_data['status'] == 'FAILED':
print(f"Transcription failed: {status_data.get('error', 'Unknown error')}")
break
time.sleep(15) # Check every 15 seconds
else:
print(f"Upload failed: {response.status_code}")
print(response.text)
```
```bash theme={null}
# Upload files for transcription
curl -X POST "https://api.audiopod.ai/api/v1/transcription/transcribe-upload" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "files=@meeting_recording.mp3" \
-F "files=@interview.wav" \
-F "language=en" \
-F "model_type=premium" \
-F "enable_speaker_diarization=true" \
-F "enable_word_timestamps=true"
# Check job status (replace JOB_ID with actual job ID)
curl -X GET "https://api.audiopod.ai/api/v1/transcription/jobs/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
# Get completed transcription
curl -X GET "https://api.audiopod.ai/api/v1/transcription/transcript/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
## Job Management
### Get Transcription Status
Check the progress and status of transcription jobs.
```http theme={null}
GET /api/v1/transcription/jobs/{job_id}
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}",
headers={"X-API-Key": api_key}
)
job_status = response.json()
print(f"Status: {job_status['status']}")
print(f"Progress: {job_status['progress']}%")
if job_status["status"] == "COMPLETED":
print(f"Transcript ready! Duration: {job_status['total_duration']} seconds")
print(f"Detected language: {job_status['detected_language']}")
print(f"Confidence: {job_status['confidence_score']}")
```
**Response (Completed):**
```json theme={null}
{
"id": 123,
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"source_urls": ["https://youtube.com/watch?v=example123"],
"language": "en",
"model_type": "premium",
"enable_speaker_diarization": true,
"min_speakers": 2,
"max_speakers": 5,
"status": "COMPLETED",
"progress": 100,
"transcript_path": "/transcripts/job_123.json",
"total_duration": 1847.5,
"detected_language": "en",
"confidence_score": 0.92,
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:45:30Z",
"estimated_credits": 150,
"display_name": "YouTube Video Transcription"
}
```
### List Transcription Jobs
Get all transcription jobs for the authenticated user.
```http theme={null}
GET /api/v1/transcription/jobs?status=COMPLETED&limit=50&offset=0
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
"https://api.audiopod.ai/api/v1/transcription/jobs",
headers={"X-API-Key": api_key},
params={
"status": "COMPLETED", # Optional filter
"limit": 50,
"offset": 0
}
)
jobs = response.json()
for job in jobs:
print(f"Job {job['id']}: {job['status']} - {job['total_duration']}s")
```
## Download Transcripts
### Get Transcript in Multiple Formats
Download transcripts in various formats including JSON, TXT, PDF, SRT, VTT, DOCX, and HTML.
```http theme={null}
GET /api/v1/transcription/jobs/{job_id}/transcript?format=json
X-API-Key: {api_key}
```
```python theme={null}
# Download as JSON with full details
response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/transcript",
headers={"X-API-Key": api_key},
params={"format": "json"}
)
transcript_data = response.json()
# Download as SRT subtitle file
response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/transcript",
headers={"X-API-Key": api_key},
params={"format": "srt"}
)
with open("transcript.srt", "w") as f:
f.write(response.text)
# Download as PDF document
response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/transcript",
headers={"X-API-Key": api_key},
params={"format": "pdf"}
)
with open("transcript.pdf", "wb") as f:
f.write(response.content)
```
**JSON Response Format:**
```json theme={null}
{
"job_id": 123,
"detected_language": "en",
"confidence_score": 0.92,
"total_duration": 1847.5,
"segments": [
{
"id": 1,
"start": 0.0,
"end": 4.5,
"text": "Welcome to our podcast about artificial intelligence.",
"confidence": 0.95,
"speaker_id": 0,
"speaker_label": "SPEAKER_00",
"words": [
{
"word": "Welcome",
"start": 0.0,
"end": 0.8,
"probability": 0.98
},
{
"word": "to",
"start": 0.8,
"end": 1.0,
"probability": 0.99
}
]
},
{
"id": 2,
"start": 5.0,
"end": 8.2,
"text": "Thank you for having me on the show.",
"confidence": 0.89,
"speaker_id": 1,
"speaker_label": "SPEAKER_01",
"words": [...]
}
],
"speakers": [
{
"id": 0,
"label": "SPEAKER_00",
"total_speaking_time": 920.3
},
{
"id": 1,
"label": "SPEAKER_01",
"total_speaking_time": 827.2
}
],
"video_metadata": [
{
"video_id": "example123",
"title": "AI Technology Discussion",
"description": "A deep dive into AI technology trends",
"duration": 1847.5,
"uploader": "Tech Channel",
"upload_date": "20240115"
}
]
}
```
## Edit Transcripts
### Get Editable Transcript
Retrieve transcript in editable format for corrections.
```http theme={null}
GET /api/v1/transcription/jobs/{job_id}/edit
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/edit",
headers={"X-API-Key": api_key}
)
editable_transcript = response.json()
print(f"Found {len(editable_transcript['segments'])} segments to edit")
```
### Update Transcript
Submit edited transcript with corrections.
```http theme={null}
PUT /api/v1/transcription/jobs/{job_id}/edit
X-API-Key: {api_key}
Content-Type: application/json
{
"segments": [
{
"id": 1,
"start": 0.0,
"end": 4.5,
"text": "Welcome to our podcast about artificial intelligence.",
"speaker_label": "SPEAKER_00",
"confidence": 0.95
}
],
"edit_notes": "Corrected technical terms and speaker labels"
}
```
```python theme={null}
# Get current transcript
response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/edit",
headers={"X-API-Key": api_key}
)
transcript = response.json()
segments = transcript["segments"]
# Make edits
segments[0]["text"] = "Welcome to our podcast about artificial intelligence."
segments[0]["speaker_label"] = "Host"
# Submit updates
response = requests.put(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/edit",
headers={"X-API-Key": api_key},
json={
"segments": segments,
"edit_notes": "Corrected speaker labels and technical terms"
}
)
if response.status_code == 200:
update_info = response.json()
print(f"Updated {update_info['changes_count']} segments")
```
### Get Transcript Versions
View edit history and versions of transcripts.
```http theme={null}
GET /api/v1/transcription/jobs/{job_id}/versions
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/versions",
headers={"X-API-Key": api_key}
)
versions = response.json()
for version in versions["versions"]:
print(f"Version {version['version']}: {version['edit_notes']}")
print(f" Updated: {version['updated_at']}")
print(f" Changes: {version['changes_count']}")
```
## Extract Audio
### Download Extracted Audio
Get clean audio files extracted from videos during transcription.
```http theme={null}
GET /api/v1/transcription/jobs/{job_id}/audio/{audio_index}
X-API-Key: {api_key}
```
```python theme={null}
# Download first audio file (index 0)
response = requests.get(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}/audio/0",
headers={"X-API-Key": api_key}
)
if response.status_code == 200:
with open("extracted_audio.wav", "wb") as f:
f.write(response.content)
print("Audio file downloaded")
```
## Delete Jobs
### Delete Transcription Job
Remove transcription jobs and associated data.
```http theme={null}
DELETE /api/v1/transcription/jobs/{job_id}
X-API-Key: {api_key}
```
```python theme={null}
response = requests.delete(
f"https://api.audiopod.ai/api/v1/transcription/jobs/{job_id}",
headers={"X-API-Key": api_key}
)
if response.status_code == 204:
print("Transcription job deleted successfully")
```
## Supported Languages
AudioPod AI supports automatic language detection or manual specification for 50+ languages:
| Language | Code | Quality | Notes |
| - | - | - | - |
| English | `en` | Excellent | Best supported language |
| Spanish | `es` | Excellent | High accuracy |
| French | `fr` | Excellent | Good speaker diarization |
| German | `de` | Excellent | Technical content support |
| Portuguese | `pt` | Very Good | Brazilian and European |
| Italian | `it` | Very Good | Good word timestamps |
| Russian | `ru` | Very Good | Cyrillic text support |
| Japanese | `ja` | Good | Hiragana/Katakana/Kanji |
| Chinese | `zh` | Good | Simplified and Traditional |
| Arabic | `ar` | Good | RTL text support |
| Hindi | `hi` | Good | Devanagari script |
| Korean | `ko` | Good | Hangul script |
## Model Comparison
Choose an accuracy tier for your use case. **Standard** is the default — just
omit `model_type`. **Premium** unlocks our highest-accuracy engine (punctuation,
per-word confidence, language detection, and native speaker labels).
| Accuracy tier | `model_type` value | Speaker labels | Best for |
| - | - | - | - |
| **Standard** (default) | *omit `model_type`* | set `enable_speaker_diarization: true` | Fast, low-cost transcription at scale |
| **Premium** | `"premium"` | set `enable_speaker_diarization: true` | Highest accuracy, punctuation, confidence |
| **Premium + speaker labels** | `"premium-diarized"` | built in | Interviews, meetings, podcasts |
Premium accuracy is a paid feature (see [Pricing](#pricing)). Standard accuracy
is available on every plan, including the free tier.
## Real-Time Streaming
Need captions as the speaker talks? Stream audio over a WebSocket and receive
**interim** (partial) results within a fraction of a second, followed by
**final**, stabilized segments with word timestamps and optional speaker labels.
```
wss://api.audiopod.ai/api/v1/transcription/stream?api_key=ap_your_key
```
Live sessions run on **Premium accuracy** and, when finished, are saved as a
normal transcription job you can fetch, edit, and export like any batch
transcript. Talk to us at [support@audiopod.ai](mailto:support@audiopod.ai) if
you need the streaming protocol for your integration.
## Best Practices
### Audio Quality Guidelines
For best transcription results:
```python theme={null}
# Recommended audio specifications
audio_requirements = {
"sample_rate": "16kHz or higher",
"format": "WAV, MP3, M4A, or video formats",
"duration": "Up to 15 hours supported",
"background_noise": "Minimize for better accuracy",
"speech_clarity": "Clear articulation preferred",
"multiple_speakers": "Distinct voices work best"
}
# Chunking for long content
chunking_strategy = {
"chunk_duration": 1800, # 30 minutes per chunk
"overlap": 30, # 30 seconds overlap
"boundary_detection": "sentence_level" # Smart chunk boundaries
}
```
### Cost Optimization
```python theme={null}
# Efficient transcription workflow
def transcribe_efficiently(audio_files, language="auto"):
# Use appropriate model based on needs
model_choice = {
"cost_priority": None, # omit model_type -> Standard (lowest cost)
"accuracy_priority": "premium",
"speaker_priority": "premium-diarized",
}
# Batch similar files together
batch_files = group_by_language_and_type(audio_files)
for batch in batch_files:
job = create_transcription_job(
files=batch,
language=language,
model_type=model_choice["accuracy_priority"],
enable_speaker_diarization=True,
chunk_duration=1800 # Optimal chunk size
)
monitor_job_progress(job["job_id"])
```
## Error Handling
**Causes:** - Unsupported audio format - Corrupted audio file - Audio too short
**Solutions:** - Use supported formats (WAV, MP3, M4A, MP4) - Verify file integrity -
Ensure minimum 10 seconds audio
**Causes:** - File size exceeds limits - Too many files in single request
**Solutions:** - Split large files into smaller chunks - Reduce number of files per request -
Use URL transcription for large videos
**Causes:** - Audio has no speech content - Extremely poor audio quality
**Solutions:** - Verify audio contains speech - Improve audio quality -
Try different transcription model
## Pricing
Transcription pricing is based on audio duration:
| Service | Cost | Description |
| - | - | - |
| Basic Transcription | 220 credits/minute | Text-only transcription |
| With Speaker Diarization | 220 credits/minute | Speaker identification included |
| With Word Timestamps | 220 credits/minute | Word-level timing data |
| Transcript Editing | Free | No additional cost for edits |
### Cost Examples
| Duration | Features | Credits | USD Cost |
| - | - | - | - |
| 10 minutes | Basic transcription | 2,200 | \$0.29 |
| 30 minutes | With speakers + timestamps | 6,600 | \$0.88 |
| 1 hour | Full features | 13,200 | \$1.76 |
| 2 hours | Full features | 26,400 | \$3.52 |
## Next Steps
Turn text into speech with a catalog or cloned voice.
Identify and separate individual speakers from audio.
Clean up audio for better transcription accuracy.
# Stem Separation
Source: https://docs.audiopod.ai/api-reference/stem-splitter
Extract individual audio components from mixed recordings using AI-powered audio source separation.
## Overview
AudioPod AI's Stem Separation API uses state-of-the-art AI models to separate mixed audio recordings into individual components (stems). Extract vocals, drums, bass, guitar, piano, and more from any song.
### Key Features
* **Granular Separation**: `two`, `four`, `six`, plus the detailed `producer`, `studio`, and `mastering` decompositions
* **Single-Instrument Isolation**: Target one instrument from a 45-instrument catalog and get it back alongside an everything-else track
* **File Upload & URL Support**: Process local files or YouTube/SoundCloud URLs
* **Quality Scores**: Get AI-assessed quality metrics for each stem
* **Presigned Downloads**: Direct S3 URLs for fast stem downloads
## Installation
```bash theme={null}
pip install audiopod
```
```bash theme={null}
npm install audiopod
```
## Authentication
Get your API key from the [API Keys Dashboard](https://www.audiopod.ai/dashboard/account/api-keys).
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Or set AUDIOPOD_API_KEY environment variable
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Or set AUDIOPOD_API_KEY environment variable
```
```bash theme={null}
export AUDIOPOD_API_KEY="ap_your_api_key"
# Use -H "X-API-Key: $AUDIOPOD_API_KEY" in requests
```
## Separation Modes
Pass one of these as `mode` to `POST /api/v1/stem-extraction/api/extract`.
| Mode | Output | Plan | Use Case |
| - | - | - | - |
| `single` | One named stem + everything-else | Basic | Vocal isolation, drum extraction |
| `two` | vocals + instrumental | Basic | Karaoke tracks |
| `four` | vocals, drums, bass, other | Basic | Standard remixing |
| `six` | + guitar, piano | Basic | Full instrument separation |
| `producer` | Drums split into kick, snare, hi-hat — up to 8 | Creator | Beat production |
| `studio` | Full drum kit plus a bass frequency split — up to 12 | Pro | Professional mixing |
| `mastering` | Full kit, bass split, and vocal stereo separation — up to 16 | Pro | Maximum detail |
**Counts are a ceiling, not a promise.** `producer`, `studio`, and `mastering`
decompose whatever is actually in the recording, so the stem count depends on
the track — a song with no cymbals yields no cymbal stem. Plan for "up to N".
**Plan gating.** `producer` requires **Creator** or above; `studio` and
`mastering` require **Pro** or above. A request from a lower plan is rejected —
call `GET /api/v1/stem-extraction/packages/available` to see what the
authenticated key can use before you dispatch a job.
For `mode="single"` this endpoint accepts the six core stems — `vocals`,
`drums`, `bass`, `guitar`, `piano`, `other`. To reach the full instrument
catalog, use the advanced endpoint below.
***
## The 45-Instrument Catalog
Beyond the fixed modes, you can name **one** instrument out of a 45-entry
catalog and get that instrument back plus an everything-else track. The 45 is
the size of the menu you can choose from — not the number of files a job
returns.
Use `POST /api/v1/stem-extraction/extract/advanced` with `single_stem` set to a
catalog id. The service resolves the right processing path and plan check for
you, so you never pick an engine.
```bash theme={null}
# Isolate the saxophone from a track
curl -X POST https://api.audiopod.ai/api/v1/stem-extraction/extract/advanced \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=VIDEO_ID" \
-F "single_stem=saxophone"
```
```bash theme={null}
# Everything EXCEPT the saxophone (JSON array, one id)
curl -X POST https://api.audiopod.ai/api/v1/stem-extraction/extract/advanced \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=VIDEO_ID" \
-F 'exclude_stems=["saxophone"]'
```
### Discover the catalog at runtime
Don't hard-code the list — plan gating and catalog contents change. `GET
/api/v1/stem-extraction/catalog` returns every stem with the plan it needs and
whether the authenticated key can use it right now:
```bash theme={null}
curl -s https://api.audiopod.ai/api/v1/stem-extraction/catalog \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
Each entry carries an `id` (what you send as `single_stem`), a `label` and
`description` for your UI, a `group`, the `public_tier` it requires, an
`allowed` boolean for the current key, and `excludable` (whether it can be used
with `exclude_stems`).
### Catalog contents
**Core — available on every plan including Basic (7)**
`vocals` · `backing_track` · `drums` · `bass` · `guitar` · `piano` · `other`
**Specialty vocals — Creator and above (4)**
`lead_vocal` · `backing_vocal` · `male_vocal` · `female_vocal`
**Instruments — Creator and above (34)**
`acoustic_guitar` · `electric_guitar` · `lead_guitar` · `rhythm_guitar` ·
`bagpipes` · `banjo` · `bassoon` · `brass` · `braam` · `celesta` · `cello` ·
`choir` · `clarinet` · `double_bass` · `flute` · `french_horn` · `fx` · `harp` ·
`harmonica` · `keys` · `mandolin` · `oboe` · `organ` · `percussion` ·
`plucked_strings` · `strings` · `saxophone` · `trombone` · `trumpet` · `tuba` ·
`viola` · `violin` · `woodwind` · `xylophone`
Requesting a Creator-tier stem with a Basic key is rejected. Check `allowed` on
the catalog response first, or handle the error and prompt for an upgrade.
**Pricing.** Catalog isolation is metered per minute of audio like every other
stem job. A few premium selections carry a multiplier — see
[API Wallet & Pricing](/account/api-wallet).
***
## Quick Start
### Six-Stem Separation
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Extract and wait for completion
result = client.stems.separate(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="six"
)
# Download stems
print("Stems ready:")
for stem, url in result["download_urls"].items():
print(f" {stem}: {url[:60]}...")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Extract and wait for completion
const result = await client.stems.separate({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'six'
});
// Download stems
console.log('Stems ready:');
for (const [stem, url] of Object.entries(result.download_urls)) {
console.log(` ${stem}: ${url.substring(0, 60)}...`);
}
```
```bash theme={null}
# Submit job
curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=VIDEO_ID" \
-F "mode=six"
# Check status (replace JOB_ID)
curl -X GET "https://api.audiopod.ai/api/v1/stem-extraction/status/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
***
## Examples by Mode
### Four-Stem Separation (Standard)
Extract vocals, drums, bass, and other.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
result = client.stems.separate(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="four"
)
print(f"Vocals: {result['download_urls']['vocals']}")
print(f"Drums: {result['download_urls']['drums']}")
print(f"Bass: {result['download_urls']['bass']}")
print(f"Other: {result['download_urls']['other']}")
```
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
result = client.stems.separate(
file="/path/to/song.mp3",
mode="four"
)
for stem, url in result["download_urls"].items():
print(f"{stem}: {url}")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
const result = await client.stems.separate({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'four'
});
console.log(`Vocals: ${result.download_urls.vocals}`);
console.log(`Drums: ${result.download_urls.drums}`);
console.log(`Bass: ${result.download_urls.bass}`);
console.log(`Other: ${result.download_urls.other}`);
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
const result = await client.stems.separate({
file: '/path/to/song.mp3',
mode: 'four'
});
for (const [stem, url] of Object.entries(result.download_urls)) {
console.log(`${stem}: ${url}`);
}
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=VIDEO_ID" \
-F "mode=four"
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "file=@/path/to/song.mp3" \
-F "mode=four"
```
***
### Single Stem Extraction (Vocals Only)
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Extract only vocals
result = client.stems.separate(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="single",
stem="vocals" # Options: vocals, drums, bass, guitar, piano, other
)
print(f"Vocals: {result['download_urls']['vocals']}")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Extract only vocals
const result = await client.stems.separate({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'single',
stem: 'vocals' // Options: vocals, drums, bass, guitar, piano, other
});
console.log(`Vocals: ${result.download_urls.vocals}`);
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=VIDEO_ID" \
-F "mode=single" \
-F "stem=vocals"
```
***
### Two-Stem Separation (Karaoke)
Separate vocals from instrumental.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
result = client.stems.separate(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="two"
)
# Returns: vocals + no_vocals (instrumental)
print(f"Vocals: {result['download_urls']['vocals']}")
print(f"Instrumental: {result['download_urls']['no_vocals']}")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
const result = await client.stems.separate({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'two'
});
// Returns: vocals + no_vocals (instrumental)
console.log(`Vocals: ${result.download_urls.vocals}`);
console.log(`Instrumental: ${result.download_urls.no_vocals}`);
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=VIDEO_ID" \
-F "mode=two"
```
***
### Producer Mode (8 Stems with Drum Kit)
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
result = client.stems.separate(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="producer"
)
# Returns: vocals, kick, snare, hihat, bass, guitar, piano, other
for stem, url in result["download_urls"].items():
print(f"{stem}: {url[:60]}...")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
const result = await client.stems.separate({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'producer'
});
// Returns: vocals, kick, snare, hihat, bass, guitar, piano, other
for (const [stem, url] of Object.entries(result.download_urls)) {
console.log(`${stem}: ${url.substring(0, 60)}...`);
}
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=VIDEO_ID" \
-F "mode=producer"
```
***
## Advanced Usage
### Async Job Handling
For more control, use `extract()` and `wait_for_completion()` separately:
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Submit job (returns immediately)
job = client.stems.extract(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="six"
)
print(f"Job submitted: {job['id']}")
# Do other work...
# Wait for completion when ready
result = client.stems.wait_for_completion(job["id"], timeout=600)
if result["status"] == "COMPLETED":
print("Download URLs:")
for stem, url in result["download_urls"].items():
print(f" {stem}: {url}")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Submit job (returns immediately)
const job = await client.stems.extract({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'six'
});
console.log(`Job submitted: ${job.id}`);
// Do other work...
// Wait for completion when ready
const result = await client.stems.waitForCompletion(job.id, 600000);
if (result.status === 'COMPLETED') {
console.log('Download URLs:');
for (const [stem, url] of Object.entries(result.download_urls)) {
console.log(` ${stem}: ${url}`);
}
}
```
### Get Available Modes
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
modes = client.stems.modes()
print("Available modes:")
for m in modes["modes"]:
print(f" {m['mode']}: {m['description']} ({m['stem_count']} stems)")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
const modes = await client.stems.modes();
console.log('Available modes:');
modes.modes.forEach(m => {
console.log(` ${m.mode}: ${m.description} (${m.stem_count} stems)`);
});
```
```bash theme={null}
curl -X GET "https://api.audiopod.ai/api/v1/stem-extraction/modes"
```
### Check Job Status
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
status = client.stems.status(5512)
print(f"Status: {status['status']}")
if status["status"] == "COMPLETED":
print("Quality scores:")
for stem, score in status["quality_scores"].items():
print(f" {stem}: {score:.2f}")
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
const status = await client.stems.status(5512);
console.log(`Status: ${status.status}`);
if (status.status === 'COMPLETED') {
console.log('Quality scores:');
for (const [stem, score] of Object.entries(status.quality_scores)) {
console.log(` ${stem}: ${score.toFixed(2)}`);
}
}
```
```bash theme={null}
curl -X GET "https://api.audiopod.ai/api/v1/stem-extraction/status/5512" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
***
## Response Format
**Completed Job Response:**
```json theme={null}
{
"id": 5512,
"status": "COMPLETED",
"source_type": "URL",
"stem_paths": {
"vocals": "stems/5512/vocals.wav",
"drums": "stems/5512/drums.wav",
"bass": "stems/5512/bass.wav",
"guitar": "stems/5512/guitar.wav",
"piano": "stems/5512/piano.wav",
"other": "stems/5512/other.wav"
},
"quality_scores": {
"vocals": 0.64,
"drums": 0.35,
"bass": 0.87,
"guitar": 0.38,
"piano": 0.0,
"other": 0.36
},
"download_urls": {
"vocals": "https://media.audiopod.ai/...",
"drums": "https://media.audiopod.ai/...",
"bass": "https://media.audiopod.ai/...",
"guitar": "https://media.audiopod.ai/...",
"piano": "https://media.audiopod.ai/...",
"other": "https://media.audiopod.ai/..."
},
"completed_at": "2025-12-11T12:39:37.379848"
}
```
***
## Pricing
**Base rate:** \$0.10 per minute of audio processed for every standard mode —
`single`, `two`, `four`, `six`, `producer`, `studio`, `mastering`.
| Duration | Cost (standard modes) |
| - | - |
| 3 minutes | \$0.30 |
| 5 minutes | \$0.50 |
| 10 minutes | \$1.00 |
| 1 hour | \$6.00 |
### Prices by output group
Different output groups add together. Selecting one or both outputs within a vocal pair costs the same. Lead and rhythm guitar also share one specialist group.
| Selected output group | Developer API wallet | Studio credits |
| - | - | - |
| Core vocals and instruments | \$0.10/min | 660/min |
| Lead + backing vocals | \$0.15/min together | 990/min together |
| Male + female vocals | \$0.15/min together | 990/min together |
| Other specialist instrument groups | \$0.20/min each | 1,320/min each |
All four vocal specialties total **$0.30/min on the API** or **1,980 Studio credits/min**. Adding core instruments totals **$0.40/min** or **2,640 Studio credits/min**. These are separate payment systems; API dollars are not a conversion of subscription credits.
Creator unlocks specialty vocals and the instrument catalog. Pro and Studio provide larger credit allowances; detailed decomposition presets require Pro. All public plans meter usage.
Read `GET /api/v1/stem-extraction/pricing` for the current group rate card. For credit-funded Studio jobs, `POST /api/v1/stem-extraction/quote` returns the measured duration, each group's price and the complete total. It does not quote API-wallet jobs. Studio quotes round duration up to whole seconds, then round the total down to whole credits; minimum charges apply. Group line items sum to that total and can differ by one credit due to allocation rounding.
Review the full quote before starting. Credits are reserved at creation. Groups that fail to deliver every requested output are refunded. A missing-output retry has its own quote; it is not permission to charge again for outputs already delivered.
Check your balance and usage at [API Wallet](/account/api-wallet).
***
## Error Handling
```python theme={null}
from audiopod import AudioPod, InsufficientBalanceError, AuthenticationError
try:
client = AudioPod(api_key="ap_...")
result = client.stems.separate(url="...", mode="six")
except AuthenticationError:
print("Invalid API key")
except InsufficientBalanceError as e:
print(f"Need ${e.required_cents / 100:.2f}, have ${e.available_cents / 100:.2f}")
except ValueError as e:
print(f"Invalid input: {e}")
```
```typescript theme={null}
import AudioPod, { InsufficientBalanceError, AuthenticationError } from 'audiopod';
try {
const client = new AudioPod({ apiKey: 'ap_...' });
const result = await client.stems.separate({ url: '...', mode: 'six' });
} catch (error) {
if (error instanceof AuthenticationError) {
console.log('Invalid API key');
} else if (error instanceof InsufficientBalanceError) {
console.log(`Need $${error.requiredCents / 100}, have $${error.availableCents / 100}`);
} else {
console.log(`Error: ${error.message}`);
}
}
```
***
## Status Codes
| Status | Description |
| - | - |
| `PENDING` | Job queued, waiting for worker |
| `PROCESSING` | Stem separation in progress |
| `COMPLETED` | Success - download URLs available |
| `FAILED` | Error occurred - check error\_message |
***
## Next Steps
Manage billing and view usage history.
Clean up stems for better quality.
# Text to Speech
Source: https://docs.audiopod.ai/api-reference/text-to-speech
Generate natural-sounding speech from text using AudioPod AI's unified voice generation technology with support for custom and standard voices.
## Overview
AudioPod AI's Text to Speech service provides unified text-to-speech capabilities for both **standard pre-built voices** and **custom voice clones**. Generate speech with any of **200+ voices** across **200+ languages** using AudioSonic, AudioPod's text-to-speech engine.
### Supported Voice Types
* **Standard Voices**: Pre-built professional voices with various characteristics
* **Custom Voices**: Your own voice clones created via [Voice Management](/api-reference/voice-management)
* **Unified API**: Same endpoint works for both voice types seamlessly with automatic routing
### Key Features
* **200+ Voices**: Pre-built voices with unique characteristics, plus your own custom clones
* **Inline Directing**: Steer emotion, delivery, non-verbal sounds, pauses, and pronunciation right inside the text — on every plan
* **Word / Character Timestamps**: Follow-along timing sidecar for captions and highlighting
* **Custom Voice Support**: Use your own voice clones and collections
* **Variable Speed Control**: Adjust speech speed from 0.25x to 4.0x
* **Multiple Formats**: MP3, WAV, OGG audio formats
* **Async Processing**: Background job processing with real-time status tracking
* **Credit Management**: Automatic credit reservation and billing
## Authentication
All endpoints require authentication. Use one of these methods:
* **API Key (Recommended)**: `X-API-Key: your_api_key` header
* **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth)
## API Endpoints Quick Reference
| Operation | Method | Endpoint |
| - | - | - |
| List voices | GET | `/api/v1/voice/voice-profiles` |
| Generate TTS | POST | `/api/v1/voice/voices/{voice_id}/generate` |
| Get job status | GET | `/api/v1/voice/tts-jobs/{job_id}/status` |
## Quick Start (cURL)
```bash theme={null}
# 1. List available voices to get a voice UUID
curl -X GET "https://api.audiopod.ai/api/v1/voice/voice-profiles?include_public=true&limit=10" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
# 2. Generate speech (replace VOICE_UUID with actual UUID from step 1)
curl -X POST "https://api.audiopod.ai/api/v1/voice/voices/VOICE_UUID/generate" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "input_text=Hello, this is a test." \
-d "audio_format=mp3" \
-d "speed=1.0"
# Returns: {"job_id": 1234, "status": "pending", ...}
# 3. Poll job status until completed
curl -X GET "https://api.audiopod.ai/api/v1/voice/tts-jobs/1234/status" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
# Returns: {"status": "completed", "output_url": "https://...", ...}
# 4. Download the audio from output_url
curl -o output.mp3 "OUTPUT_URL_FROM_STEP_3"
```
## Generate Speech
### Basic Text to Speech
Generate speech from text using any voice (standard or custom) by voice UUID, ID, or name. All generation is processed asynchronously with job tracking.
```python theme={null}
from audiopod import Client
import requests
import time
# Initialize client
client = Client()
# Generate speech using voice UUID
job = client.voice.generate_speech(
voice_id="550e8400-e29b-41d4-a716-446655440000", # Voice UUID
text="Hello! This is AudioPod AI generating natural speech.",
audio_format="mp3",
language="en",
speed=1.0
)
print(f"Generation job created: {job.id}")
print(f"Status: {job.status}")
# Check job status until completion
while True:
status = client.voice.get_job_status(job.id)
print(f"Job status: {status.status}")
if status.status == 'completed':
# Get output URL from job result
if status.result and 'output_url' in status.result:
audio_url = status.result['output_url']
print(f"Audio ready: {audio_url}")
# Download the audio file
audio_response = requests.get(audio_url)
with open("generated_speech.mp3", "wb") as f:
f.write(audio_response.content)
print("Audio saved as generated_speech.mp3")
break
elif status.status == 'failed':
print(f"Job failed: {status.error_message}")
break
time.sleep(2) # Wait 2 seconds before checking again
# Alternative: Generate using voice name (for standard voices)
job = client.voice.generate_speech(
voice_id="aura", # Voice name
text="Hello! This uses a standard voice by name.",
audio_format="mp3",
speed=1.0
)
```
```javascript theme={null}
const { AudioPodClient } = require('audiopod-js');
const fs = require('fs');
// Initialize client
const client = new AudioPodClient();
async function generateSpeech() {
try {
// Generate speech using voice UUID
const job = await client.voice.generateSpeech(
'550e8400-e29b-41d4-a716-446655440000', // Voice ID/UUID
'Hello! This is AudioPod AI generating natural speech.', // Text
{
language: 'en',
audioFormat: 'mp3',
generationParams: {
speed: 1.0
}
}
);
console.log(`Generation job created: ${job.id}`);
console.log(`Status: ${job.status}`);
// Check job status until completion
while (true) {
const status = await client.voice.getJobStatus(job.id);
console.log(`Job status: ${status.status}`);
if (status.status === 'completed') {
const audioUrl = status.output_url;
console.log(`Audio ready: ${audioUrl}`);
// Download the audio file
const fetch = require('node-fetch');
const audioResponse = await fetch(audioUrl);
const buffer = await audioResponse.buffer();
fs.writeFileSync('generated_speech.mp3', buffer);
console.log('Audio saved as generated_speech.mp3');
break;
} else if (status.status === 'failed') {
console.log(`Job failed: ${status.error_message}`);
break;
}
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds
}
} catch (error) {
console.error('Error:', error.message);
}
}
generateSpeech();
```
```python theme={null}
import requests
import time
# Create TTS job using voice UUID
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voices/550e8400-e29b-41d4-a716-446655440000/generate",
headers={"X-API-Key": api_key},
data={
"input_text": "Hello! This is AudioPod AI generating natural speech.",
"audio_format": "mp3",
"speed": 1.0,
"language": "en"
}
)
if response.status_code == 200:
job_data = response.json()
job_id = job_data["job_id"]
print(f"Voice generation job created: {job_id}")
print(f"Credits reserved: {job_data.get('credits_reserved')}")
# Poll job status until completion
while True:
status_response = requests.get(
f"https://api.audiopod.ai/api/v1/voice/tts-jobs/{job_id}/status",
headers={"X-API-Key": api_key}
)
if status_response.status_code == 200:
status_data = status_response.json()
print(f"Job status: {status_data['status']}")
if status_data['status'] == 'completed':
audio_url = status_data['output_url']
print(f"Audio ready: {audio_url}")
# Download the audio file
audio_response = requests.get(audio_url)
with open("generated_speech.mp3", "wb") as f:
f.write(audio_response.content)
print("Audio saved as generated_speech.mp3")
break
elif status_data['status'] == 'failed':
print(f"Job failed: {status_data.get('error_message')}")
break
time.sleep(2) # Wait 2 seconds before checking again
```
```bash theme={null}
# Create TTS job using voice UUID
JOB_RESPONSE=$(curl -s -X POST "https://api.audiopod.ai/api/v1/voice/voices/550e8400-e29b-41d4-a716-446655440000/generate" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'input_text=Hello! This is AudioPod AI generating natural speech.' \
-d 'audio_format=mp3' \
-d 'speed=1.0' \
-d 'language=en')
# Extract job ID
JOB_ID=$(echo $JOB_RESPONSE | jq -r '.job_id')
echo "Job created: $JOB_ID"
# Poll job status
while true; do
STATUS_RESPONSE=$(curl -s -X GET "https://api.audiopod.ai/api/v1/voice/tts-jobs/$JOB_ID/status" \
-H "X-API-Key: $AUDIOPOD_API_KEY")
STATUS=$(echo $STATUS_RESPONSE | jq -r '.status')
echo "Job status: $STATUS"
if [ "$STATUS" = "completed" ]; then
AUDIO_URL=$(echo $STATUS_RESPONSE | jq -r '.output_url')
echo "Audio ready: $AUDIO_URL"
# Download the audio
curl -o generated_speech.mp3 "$AUDIO_URL"
echo "Audio saved as generated_speech.mp3"
break
elif [ "$STATUS" = "failed" ]; then
echo "Job failed"
break
fi
sleep 2
done
```
**Parameters** (submit as `application/x-www-form-urlencoded` form fields):
* `voice_id` (required): Voice UUID, ID, or name from your voice collection (path parameter)
* `input_text` (required): Text to convert to speech (max **20,000 characters** per request; may include inline directing — see below)
* `audio_format` (optional): Output format - mp3, wav, ogg (default: mp3)
* `speed` (optional): Speech speed 0.25-4.0 (default: 1.0)
* `language` (optional): Language code - auto-detected if not provided
* `generation_params` (optional): JSON string of premium-voice controls — `delivery_mode`, `timestamp_type`, `text_normalization` (see [Delivery & timestamp controls](#delivery-timestamp-controls))
**Response:**
```json theme={null}
{
"job_id": 12345,
"status": "pending",
"message": "Voice generation job created successfully",
"voice_id": 123,
"estimated_duration": 30,
"credits_reserved": 25
}
```
## Inline Directing
Steer the performance directly inside `input_text` — no separate parameters
needed. Inline directing is available on **every plan**, including the free tier.
### Emotion & delivery cues
Lead a segment with a bracketed direction of one or two words. It sets the mood
for the text that follows, until the next cue.
```text theme={null}
[whispering, tense] Did you hear that? [excited] It's finally here!
```
### Non-verbal sounds
Drop a bracketed tag where you want a natural, non-verbal sound:
```text theme={null}
That's hilarious [laugh]. Okay [sigh], let's get back to it.
```
Supported tags: `[laugh]` `[sigh]` `[clear throat]` `[breathe]` `[cough]`
`[yawn]` `[chuckle]` `[gasp]` `[groan]`.
### Pauses
Insert a timed silence with a self-closing break tag. Each pause is capped at
**10 seconds**, and you can use up to **20 breaks per request**.
```text theme={null}
Let me think about that yes, that works.
```
### Pronunciation (inline IPA)
Wrap a word's [IPA](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet)
pronunciation in forward slashes to override how it's spoken:
```text theme={null}
Welcome to /ˈwʊstər/ — you'd never guess it from the spelling.
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/voice/voices/VOICE_UUID/generate" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode 'input_text=[warm] Hello there [laugh]. Welcome to /ˈwʊstər/ let'\''s begin.' \
-d 'audio_format=mp3'
```
```python theme={null}
import requests
directed_text = (
"[warm] Hello there [laugh]. "
'Welcome to /ˈwʊstər/ '
"let's begin."
)
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voices/VOICE_UUID/generate",
headers={"X-API-Key": api_key},
data={"input_text": directed_text, "audio_format": "mp3"},
)
print(response.json()["job_id"])
```
## Delivery & timestamp controls
Pass a JSON string in the `generation_params` form field to tune delivery and
request a timestamp sidecar.
| Key | Values | Description |
| - | - | - |
| `delivery_mode` | `stable`, `balanced`, `creative` | How expressive vs. consistent the read is. `stable` hugs the text closely; `creative` gives the voice more range. |
| `timestamp_type` | `word`, `character` | Emit follow-along timestamps at word or character granularity. |
| `text_normalization` | `true`, `false` | Expand numbers, dates, and abbreviations into spoken form. |
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/voice/voices/VOICE_UUID/generate" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode 'input_text=The meeting is at 3:30 on 04/09.' \
--data-urlencode 'generation_params={"delivery_mode":"balanced","timestamp_type":"word","text_normalization":true}'
```
### Timestamps
When you request `timestamp_type`, the completed job response includes a
`timestamps_url` — a short-lived presigned URL to a sidecar file with per-word
(or per-character) timing. Use it to drive live captions, karaoke-style
highlighting, or word-accurate video alignment.
```json theme={null}
{
"id": 12345,
"status": "completed",
"output_url": "https://presigned-url-to-audio-file",
"timestamps_url": "https://presigned-url-to-timestamps-sidecar.json",
"total_duration": 4.2
}
```
Each entry pairs a token with its `start`/`end` time in seconds:
```json theme={null}
{
"type": "word",
"tokens": [
{ "text": "The", "start": 0.00, "end": 0.18 },
{ "text": "meeting", "start": 0.18, "end": 0.61 }
]
}
```
### Voice Identification Examples
```python theme={null}
from audiopod import Client
client = Client()
# Using standard pre-built voice by name
job1 = client.voice.generate_speech(
voice_identifier="aura", # Standard voice name
input_text="Hello! This uses a standard pre-built voice.",
audio_format="mp3",
speed=1.0
)
print(f"Standard voice job: {job1.job_id}")
# Using custom voice clone (by integer ID)
job2 = client.voice.generate_speech(
voice_identifier=123, # Your custom voice ID
input_text="Hello! This uses my custom cloned voice.",
audio_format="mp3",
speed=1.0
)
print(f"Custom voice job: {job2.job_id}")
# Using voice via UUID (works for both standard and custom voices)
job3 = client.voice.generate_speech(
voice_identifier="550e8400-e29b-41d4-a716-446655440000", # Voice UUID
input_text="Hello! This uses voice identification via UUID.",
audio_format="mp3",
speed=1.0
)
print(f"UUID voice job: {job3.job_id}")
# List available voices
voices = client.voice.list_voices(include_public=True)
print("Available voices:")
for voice in voices:
print(f"- {voice.name} (UUID: {voice.uuid}): {voice.description}")
```
```javascript theme={null}
const { AudioPodClient } = require('audiopod-js');
const client = new AudioPodClient();
async function demonstrateVoiceIdentification() {
try {
// Using standard pre-built voice by name
const job1 = await client.voice.generateSpeech({
voiceIdentifier: 'aura', // Standard voice name
inputText: 'Hello! This uses a standard pre-built voice.',
audioFormat: 'mp3',
speed: 1.0
});
console.log(`Standard voice job: ${job1.jobId}`);
// Using custom voice clone (by integer ID)
const job2 = await client.voice.generateSpeech({
voiceIdentifier: 123, // Your custom voice ID
inputText: 'Hello! This uses my custom cloned voice.',
audioFormat: 'mp3',
speed: 1.0
});
console.log(`Custom voice job: ${job2.jobId}`);
// Using voice via UUID (works for both standard and custom voices)
const job3 = await client.voice.generateSpeech({
voiceIdentifier: '550e8400-e29b-41d4-a716-446655440000', // Voice UUID
inputText: 'Hello! This uses voice identification via UUID.',
audioFormat: 'mp3',
speed: 1.0
});
console.log(`UUID voice job: ${job3.jobId}`);
// List available voices
const voices = await client.voice.listVoices({ includePublic: true });
console.log('Available voices:');
voices.forEach(voice => {
console.log(`- ${voice.name} (UUID: ${voice.uuid}): ${voice.description}`);
});
} catch (error) {
console.error('Error:', error.message);
}
}
demonstrateVoiceIdentification();
```
```python theme={null}
import requests
headers = {"X-API-Key": api_key}
# Generate with a standard voice by name
response1 = requests.post(
"https://api.audiopod.ai/api/v1/voice/voices/aura/generate",
headers=headers,
data={
"input_text": "Hello! This uses a standard pre-built voice.",
"audio_format": "mp3",
"speed": 1.0
}
)
print(f"Standard voice job: {response1.json()['job_id']}")
# Generate with your custom voice by ID
response2 = requests.post(
"https://api.audiopod.ai/api/v1/voice/voices/123/generate", # Your custom voice ID
headers=headers,
data={
"input_text": "Hello! This uses my custom cloned voice.",
"audio_format": "mp3",
"speed": 1.0
}
)
print(f"Custom voice job: {response2.json()['job_id']}")
# Generate with voice using UUID
response3 = requests.post(
"https://api.audiopod.ai/api/v1/voice/voices/550e8400-e29b-41d4-a716-446655440000/generate",
headers=headers,
data={
"input_text": "Hello! This uses voice identification via UUID.",
"audio_format": "mp3",
"speed": 1.0
}
)
print(f"UUID voice job: {response3.json()['job_id']}")
# List available voices
voices_response = requests.get(
"https://api.audiopod.ai/api/v1/voice/voice-profiles?include_public=true",
headers=headers
)
voices = voices_response.json()
print("Available voices:")
for voice in voices:
print(f"- {voice['name']} (UUID: {voice['uuid']}): {voice['description']}")
```
## Available Voices
AudioPod AI offers a diverse collection of voices including both standard pre-built voices and custom voice clones:
### Voice Types
* **Standard Voices**: Professional pre-built voices with unique characteristics
* **Custom Voices**: Your own voice clones created from audio samples
* **Public Voices**: Community-shared voices available to all users
### Popular Standard Voices
| Voice Name | Gender | Languages | Style | Description |
| - | - | - | - | - |
| `aura` | Female | 60+ | Bright | Luminous voice with crystal-clear delivery |
| `jester` | Male | 60+ | Playful | Upbeat voice with theatrical flair |
| `sage` | Male | 60+ | Wise | Authoritative voice perfect for narration |
| `ava` | Female | 60+ | Professional | Commanding voice for business content |
| `surge` | Male | 60+ | Energetic | High-energy voice for exciting content |
| `willow` | Female | 60+ | Gentle | Delicate, youthful voice with elegance |
### Listing Available Voices
Use the voice profiles endpoint to discover all available voices:
```python theme={null}
# List all voices (including your custom voices)
voices = client.voice.list_voices(include_public=True)
# Filter by voice type
standard_voices = [v for v in voices if v.voice_type == "standard"]
custom_voices = [v for v in voices if v.voice_type == "custom"]
print(f"Found {len(standard_voices)} standard voices")
print(f"Found {len(custom_voices)} custom voices")
```
## Job Status Tracking
### Check Job Status
Monitor your text-to-speech generation jobs with real-time status updates:
```python theme={null}
# Check job status
status = client.voice.get_job_status(job_id)
print(f"Job {job_id} status: {status['status']}")
print(f"Progress: {status.get('progress', 'N/A')}%")
if status['status'] == 'completed':
print(f"Audio ready: {status['output_url']}")
print(f"Duration: {status.get('total_duration')} seconds")
elif status['status'] == 'failed':
print(f"Error: {status.get('error_message')}")
```
### Job Status Response
```json theme={null}
{
"id": 12345,
"job_type": "single",
"user_id": "user-uuid",
"status": "completed",
"progress": 100,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:45Z",
"completed_at": "2024-01-15T10:30:45Z",
"output_path": "generated/12345.mp3",
"output_url": "https://presigned-url-to-audio-file",
"input_text": "Hello! This is AudioPod AI generating natural speech.",
"target_language": "en",
"voice_info": {
"id": 123,
"name": "aura",
"display_name": "Aura"
},
"estimated_duration": 30,
"credits_reserved": 25
}
```
### Status Values
* `pending`: Job created and waiting for processing
* `processing`: Currently generating audio
* `completed`: Audio generation finished successfully
* `failed`: Generation failed with error
## Configuration Options
### Audio Format Options
| Parameter | Options | Description | Notes |
| - | - | - | - |
| `audio_format` | `mp3`, `wav`, `ogg` | Audio file format | MP3 recommended for most uses |
| `speed` | `0.25` to `4.0` | Speech speed | 1.0 = normal speed |
| `language` | Language codes | Target language | Auto-detected if not specified |
### Basic Voice Customization
```python theme={null}
# Generate with custom settings
job = client.voice.generate_speech(
voice_identifier="aura",
input_text="Customized voice output with speed control",
audio_format="wav", # High quality WAV format
speed=1.1, # 10% faster than normal
language="en" # English language
)
```
### Language Detection
AudioPod AI automatically detects the language of your input text, but you can specify it explicitly for better results:
```python theme={null}
# Automatic language detection
job1 = client.voice.generate_speech(
voice_identifier="aura",
input_text="Hello, how are you today?"
# Language will be auto-detected as English
)
# Explicit language specification
job2 = client.voice.generate_speech(
voice_identifier="aura",
input_text="Bonjour, comment allez-vous?",
language="fr" # Specify French
)
```
## Multi-Language Support
### Language Codes
AudioPod AI supports 100+ languages with automatic detection and consistent quality across all supported languages:
| Language | Code | Description |
| - | - | - |
| English (US) | `en-US` | American English |
| English (UK) | `en-GB` | British English |
| Spanish | `es` | Spanish (Spain) |
| Spanish (Mexico) | `es-MX` | Mexican Spanish |
| French | `fr` | French (France) |
| German | `de` | German |
| Chinese (Simplified) | `zh-CN` | Simplified Chinese (Mandarin) |
| Chinese (Traditional) | `zh-TW` | Traditional Chinese |
| Japanese | `ja` | Japanese |
| Korean | `ko` | Korean |
| Portuguese | `pt` | Portuguese (Portugal) |
| Portuguese (Brazil) | `pt-BR` | Brazilian Portuguese |
| Russian | `ru` | Russian |
| Arabic | `ar` | Arabic |
| Hindi | `hi` | Hindi |
| Italian | `it` | Italian |
| Dutch | `nl` | Dutch |
| Polish | `pl` | Polish |
| Turkish | `tr` | Turkish |
| Swedish | `sv` | Swedish |
### Multi-Language Example
```python theme={null}
# Generate speech in multiple languages using the same voice
languages = [
{"text": "Hello, welcome to AudioPod AI", "lang": "en-US"},
{"text": "Hola, bienvenido a AudioPod AI", "lang": "es"},
{"text": "Bonjour, bienvenue chez AudioPod AI", "lang": "fr"},
{"text": "Hallo, willkommen bei AudioPod AI", "lang": "de"}
]
voice_uuid = "550e8400-e29b-41d4-a716-446655440000" # Your multi-language voice
for item in languages:
job = client.voice.generate_speech(
voice_identifier=voice_uuid,
input_text=item["text"],
language=item["lang"],
audio_format="mp3"
)
# Wait for completion and download
while True:
status = client.voice.get_job_status(job.job_id)
if status['status'] == 'completed':
# Download audio
audio_response = requests.get(status['output_url'])
filename = f"welcome_{item['lang']}.mp3"
with open(filename, "wb") as f:
f.write(audio_response.content)
print(f"Generated {filename}")
break
elif status['status'] == 'failed':
print(f"Failed to generate {item['lang']}: {status.get('error_message')}")
break
time.sleep(2)
```
### Get Supported Languages for a Voice
```python theme={null}
# Get supported languages for a specific voice
supported_languages = client.voice.get_supported_languages(voice_identifier="aura")
print(f"Voice supports {len(supported_languages)} languages:")
for code, name in supported_languages.items():
print(f"- {code}: {name}")
```
## Use Cases & Examples
### Audiobook Narration
```python theme={null}
from audiopod import Client
import re
client = Client()
def split_text_by_sentences(text, max_length=2000):
"""Split text into chunks by sentences, respecting max length"""
sentences = re.split(r'[.!?]+', text)
chunks = []
current_chunk = ""
for sentence in sentences:
if len(current_chunk + sentence) > max_length:
if current_chunk:
chunks.append(current_chunk.strip())
current_chunk = sentence
else:
current_chunk += sentence + ". "
if current_chunk:
chunks.append(current_chunk.strip())
return chunks
def create_audiobook_chapter(chapter_text, narrator_voice="sage"):
"""Create audiobook chapter with professional narration"""
# Split long text into manageable chunks
chunks = split_text_by_sentences(chapter_text, max_length=2000)
audio_urls = []
for i, chunk in enumerate(chunks):
print(f"Processing chunk {i+1}/{len(chunks)}: {chunk[:50]}...")
result = client.voice.generate_speech(
voice_id=narrator_voice,
text=chunk,
audio_format="wav", # High quality for audiobooks
speed=0.95, # Slightly slower for audiobooks
wait_for_completion=True
)
audio_urls.append(result.output_url)
print(f"Generated audio for chunk {i+1}")
return audio_urls
# Example usage
chapter_text = """
Chapter 1: The Beginning
It was the best of times, it was the worst of times. The era was filled with
contradictions that would shape the destiny of nations. In this tumultuous
period, heroes would rise and fall, love would conquer fear, and the very
fabric of society would be tested by forces beyond imagination.
As dawn broke over the ancient city, our protagonist began a journey that
would change everything they thought they knew about the world.
"""
audio_urls = create_audiobook_chapter(chapter_text, "sage")
print(f"Audiobook chapter complete! Generated {len(audio_urls)} audio segments.")
```
```javascript theme={null}
const { AudioPodClient } = require('audiopod-js');
const client = new AudioPodClient();
function splitTextBySentences(text, maxLength = 2000) {
const sentences = text.split(/[.!?]+/);
const chunks = [];
let currentChunk = '';
for (const sentence of sentences) {
if ((currentChunk + sentence).length > maxLength) {
if (currentChunk) {
chunks.push(currentChunk.trim());
}
currentChunk = sentence;
} else {
currentChunk += sentence + '. ';
}
}
if (currentChunk) {
chunks.push(currentChunk.trim());
}
return chunks;
}
async function createAudiobookChapter(chapterText, narratorVoice = 'sage') {
try {
// Split long text into manageable chunks
const chunks = splitTextBySentences(chapterText, 2000);
const audioUrls = [];
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
console.log(`Processing chunk ${i+1}/${chunks.length}: ${chunk.substring(0, 50)}...`);
const result = await client.voice.generateSpeech({
voiceId: narratorVoice,
text: chunk,
audioFormat: 'wav', // High quality for audiobooks
speed: 0.95, // Slightly slower for audiobooks
waitForCompletion: true
});
audioUrls.push(result.outputUrl);
console.log(`Generated audio for chunk ${i+1}`);
}
return audioUrls;
} catch (error) {
console.error('Error creating audiobook:', error.message);
throw error;
}
}
// Example usage
const chapterText = `
Chapter 1: The Beginning
It was the best of times, it was the worst of times. The era was filled with
contradictions that would shape the destiny of nations. In this tumultuous
period, heroes would rise and fall, love would conquer fear, and the very
fabric of society would be tested by forces beyond imagination.
As dawn broke over the ancient city, our protagonist began a journey that
would change everything they thought they knew about the world.
`;
createAudiobookChapter(chapterText, 'sage')
.then(audioUrls => {
console.log(`Audiobook chapter complete! Generated ${audioUrls.length} audio segments.`);
audioUrls.forEach((url, index) => {
console.log(`Segment ${index + 1}: ${url}`);
});
})
.catch(error => {
console.error('Failed to create audiobook:', error);
});
```
```python theme={null}
from audiopod import Client
import asyncio
import aiohttp
import json
client = Client()
async def create_audiobook_chapter_parallel(chapter_text, narrator_voice="sage"):
"""Create audiobook with parallel processing for faster generation"""
chunks = split_text_by_sentences(chapter_text, max_length=2000)
# Create all jobs first (parallel creation)
jobs = []
for chunk in chunks:
job = client.voice.generate_speech(
voice_id=narrator_voice,
text=chunk,
audio_format="wav",
speed=0.95,
wait_for_completion=False # Don't wait, create jobs in parallel
)
jobs.append(job)
print(f"Created {len(jobs)} generation jobs")
# Wait for all jobs to complete
audio_urls = []
for i, job in enumerate(jobs):
print(f"Waiting for job {i+1}/{len(jobs)}")
# Poll job status
while True:
status = client.voice.get_job_status(job.id)
if status.status == 'completed':
audio_urls.append(status.output_url)
break
elif status.status == 'failed':
print(f"Job {job.id} failed: {status.error_message}")
break
await asyncio.sleep(2)
return audio_urls
# Example with error handling and progress tracking
def create_audiobook_with_progress(chapter_text, narrator_voice="sage"):
"""Create audiobook with detailed progress tracking"""
chunks = split_text_by_sentences(chapter_text, max_length=2000)
total_chunks = len(chunks)
completed_chunks = 0
audio_urls = []
print(f"Starting audiobook generation: {total_chunks} chunks")
for i, chunk in enumerate(chunks):
try:
print(f"[{i+1}/{total_chunks}] Processing: {chunk[:50]}...")
result = client.voice.generate_speech(
voice_id=narrator_voice,
text=chunk,
audio_format="wav",
speed=0.95,
wait_for_completion=True
)
audio_urls.append(result.output_url)
completed_chunks += 1
progress = (completed_chunks / total_chunks) * 100
print(f"[{i+1}/{total_chunks}] ✅ Complete ({progress:.1f}%)")
except Exception as e:
print(f"[{i+1}/{total_chunks}] ❌ Failed: {e}")
# You could implement retry logic here
print(f"Audiobook generation complete! {completed_chunks}/{total_chunks} chunks successful")
return audio_urls
```
### Podcast Introduction
Direct the delivery with **inline markup** — a leading `[direction]`, timed
`` pauses, and (optionally) `generation_params`. No SSML.
```python theme={null}
intro_script = (
"[speak with warm upbeat energy] Welcome back to the Tech Talk Podcast! "
' '
"Today we're diving deep into artificial intelligence and its impact on "
'content creation. Let\'s get started!'
)
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voices/ava/generate",
headers={"Authorization": f"Bearer {API_KEY}"},
data={
"input_text": intro_script,
"audio_format": "mp3",
# Expressive read; see "Delivery & timestamp controls" above.
"generation_params": '{"delivery_mode":"creative"}',
},
)
job_id = response.json()["job_id"] # poll to COMPLETED, then download audio_url
```
### E-Learning Content
```python theme={null}
def create_lesson_audio(lesson_content):
# Direct the pacing with a leading note + timed pauses (not SSML).
directed = (
"[speak clearly and calmly at a measured pace] Lesson begins now. "
' '
f"{lesson_content} "
' '
"That concludes this lesson. Take a moment to review what you've learned."
)
return requests.post(
"https://api.audiopod.ai/api/v1/voice/voices/sage/generate",
headers={"Authorization": f"Bearer {API_KEY}"},
data={
"input_text": directed,
"audio_format": "mp3",
"generation_params": '{"delivery_mode":"stable"}',
},
)
```
### Interactive Voice Response (IVR)
```python theme={null}
ivr_prompts = {
"welcome": "Thank you for calling AudioPod AI. Your call is important to us.",
"menu": "Press 1 for sales, 2 for support, or 3 for billing.",
"hold": "Please hold while we connect you to the next available agent.",
}
for prompt_name, text in ivr_prompts.items():
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voices/ava/generate",
headers={"Authorization": f"Bearer {API_KEY}"},
data={"input_text": text, "audio_format": "wav"},
)
# Poll response.json()["job_id"] to COMPLETED, then download its audio_url.
```
## Best Practices
### Text Optimization
**✅ Good Practices:**
* Use proper punctuation for natural pauses
* Write numbers in word form for better pronunciation
* Include context for abbreviations
* Break long sentences into shorter ones
**❌ Common Issues:**
* ALL CAPS TEXT (sounds like shouting)
* Missing punctuation (unnatural flow)
* Technical jargon without context
* Extremely long paragraphs
### Cost Optimization
```python theme={null}
# Efficient: Single request for multiple sentences
long_text = "First sentence. Second sentence. Third sentence."
job = client.voice.generate_speech(
voice_identifier="aura",
input_text=long_text,
audio_format="mp3"
)
# Inefficient: Multiple requests for short texts
# This creates multiple jobs and uses more credits due to per-job overhead
sentences = ["First sentence.", "Second sentence.", "Third sentence."]
for sentence in sentences:
job = client.voice.generate_speech(
voice_identifier="aura",
input_text=sentence,
audio_format="mp3"
)
```
### Caching Strategy
```python theme={null}
import hashlib
import os
import requests
import time
def get_cached_audio(text, voice_identifier, cache_dir="audio_cache"):
# Create cache key from text and voice identifier
cache_key = hashlib.md5(f"{text}_{voice_identifier}".encode()).hexdigest()
cache_file = os.path.join(cache_dir, f"{cache_key}.mp3")
if os.path.exists(cache_file):
with open(cache_file, "rb") as f:
return f.read()
# Generate new audio using job-based API
job = client.voice.generate_speech(
voice_identifier=voice_identifier,
input_text=text,
audio_format="mp3"
)
# Wait for completion
while True:
status = client.voice.get_job_status(job.job_id)
if status['status'] == 'completed':
# Download audio
audio_response = requests.get(status['output_url'])
audio_data = audio_response.content
# Cache the result
os.makedirs(cache_dir, exist_ok=True)
with open(cache_file, "wb") as f:
f.write(audio_data)
return audio_data
elif status['status'] == 'failed':
raise Exception(f"Audio generation failed: {status.get('error_message')}")
time.sleep(2)
```
## Error Handling
### Common Errors and Solutions
**Causes:** - Text too long (>20,000 characters) - Invalid characters or
encoding - Empty text field - Malformed `generation_params` JSON **Solutions:** - Split long text into chunks -
Check text encoding (UTF-8) - Validate text is not empty - Verify `generation_params` is a valid JSON object
**Causes:** - Voice identifier doesn't exist - Voice not accessible by user
* Voice UUID format invalid **Solutions:** - Check available voices with voice profiles endpoint -
Verify voice UUID format - Ensure voice is public or owned by user
**Causes:** - Not enough credits for audio generation - Credit limit exceeded
**Solutions:** - Check credit balance - Purchase more credits - Wait for credit reset
**Causes:** - Rate limit exceeded - Too many concurrent requests
**Solutions:** - Implement exponential backoff - Use request queuing -
Upgrade to higher rate limits
### Robust Error Handling
```python theme={null}
import time
import random
import requests
from requests.exceptions import HTTPError
def generate_speech_with_retry(text, voice_identifier, max_retries=3):
for attempt in range(max_retries):
try:
# Create generation job
job = client.voice.generate_speech(
voice_identifier=voice_identifier,
input_text=text,
audio_format="mp3"
)
# Monitor job with retry logic for status checks
while True:
try:
status = client.voice.get_job_status(job.job_id)
if status['status'] == 'completed':
# Download and return audio
audio_response = requests.get(status['output_url'])
audio_response.raise_for_status()
return audio_response.content
elif status['status'] == 'failed':
raise Exception(f"Job failed: {status.get('error_message')}")
time.sleep(2) # Wait before checking again
except HTTPError as e:
if e.response.status_code == 429:
# Rate limit on status check - wait longer
time.sleep(5)
continue
else:
raise
except HTTPError as e:
if e.response.status_code == 429: # Rate limit
# Exponential backoff with jitter
delay = (2 ** attempt) + random.uniform(0, 1)
print(f"Rate limited, retrying in {delay:.1f}s (attempt {attempt + 1})")
time.sleep(delay)
continue
elif e.response.status_code == 400:
# Bad request - don't retry
print(f"Bad request: {e.response.text}")
raise e
elif e.response.status_code == 402:
# Insufficient credits - don't retry
print("Insufficient credits")
raise e
elif e.response.status_code == 404:
# Voice not found - don't retry
print(f"Voice not found: {voice_identifier}")
raise e
else:
# Other errors - retry with delay
print(f"Error {e.response.status_code}, retrying...")
time.sleep(1)
continue
except Exception as e:
print(f"Unexpected error: {e}")
time.sleep(1)
continue
raise Exception("Max retries exceeded")
# Usage example
try:
audio_data = generate_speech_with_retry(
text="Hello, this is a test with retry logic.",
voice_identifier="aura"
)
with open("output.mp3", "wb") as f:
f.write(audio_data)
print("Audio generated successfully with retry logic")
except Exception as e:
print(f"Failed to generate audio: {e}")
```
## Pricing
Text to Speech is billed differently depending on how you authenticate:
| Path | Rate | Basis |
| - | - | - |
| **Account credits** (subscription / JWT) | **0.5 credits per character** | Input character count, charged before synthesis |
| **API wallet** (API key, USD) | **\$0.05 per 1,000 characters** | Input character count, prepaid; minimum \$0.01 per request |
All voice types (standard and custom) use the same rate. Quality settings do
not affect pricing.
### Cost Examples — account credits (per character)
| Text Length | Credits Used |
| - | - |
| 100 characters | 50 credits |
| 500 characters | 250 credits |
| 1,000 characters | 500 credits |
| 5,000 characters | 2,500 credits |
### Cost Examples — API wallet (per character)
| Text Length | USD Cost |
| - | - |
| 100 characters | \$0.01 (minimum) |
| 1,000 characters | \$0.05 |
| 10,000 characters | \$0.50 |
| 100,000 characters | \$5.00 |
Directing/steering tags and pause tags are not counted; non-verbal tags such as
`[laugh]` are. See [How characters are counted](/account/api-wallet#how-characters-are-counted).
## Next Steps
Create custom voices from audio samples.
Listen to voice samples and find your perfect voice.
# Voice Changer
Source: https://docs.audiopod.ai/api-reference/voice-changer
Transform voice characteristics in audio recordings using AudioPod AI's advanced voice-to-voice conversion technology.
## Overview
AudioPod AI's Voice Changer API converts source audio to match target voice characteristics. Transform any speech recording to sound like a different voice while preserving the original speech content, timing, and emotional expression.
### Key Features
* **Voice Conversion**: Transform source audio to match target voice characteristics
* **Content Preservation**: Maintains original speech content, timing, and emotional expression
* **Multiple Voice Sources**: Use any completed voice profile as a target voice
* **High Quality Processing**: Advanced voice-to-voice conversion for natural-sounding results
* **Public Voice Support**: Access to both user-owned and public voice profiles
* **Flexible Input**: Support for various audio formats (WAV, MP3, M4A, etc.)
* **Real-time Processing**: Fast conversion for production workflows
## Authentication
All endpoints require authentication:
* **API Key (Recommended)**: `X-API-Key: your_api_key` header
* **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth)
## Voice Conversion
### Convert Audio to Target Voice
Transform the voice characteristics in a source audio file to match a target voice profile.
```http theme={null}
POST /api/v1/voice/voice-convert
X-API-Key: {api_key}
Content-Type: multipart/form-data
file: (source audio file)
voice_uuid: "550e8400-e29b-41d4-a716-446655440000"
```
```python theme={null}
import requests
# Convert audio to match target voice
with open("source_speech.wav", "rb") as audio_file:
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voice-convert",
headers={"X-API-Key": api_key},
data={
"voice_uuid": "550e8400-e29b-41d4-a716-446655440000" # Target voice UUID
},
files={"file": audio_file}
)
if response.status_code == 200:
conversion_job = response.json()
job_id = conversion_job["id"]
print(f"Voice conversion job created: {job_id}")
print(f"Status: {conversion_job['status']}")
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/voice/voice-convert" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "file=@source_speech.wav" \
-F "voice_uuid=550e8400-e29b-41d4-a716-446655440000"
```
**Parameters:**
* **`file`** (required): Source audio file containing speech to convert
* **`voice_uuid`** (required): UUID of the target voice profile to match
**Voice UUID Sources:**
* User's own custom voice profiles (from voice cloning)
* Public voice profiles available in the voice library
* Voice must have status "COMPLETED" and available audio file
**Response:**
```json theme={null}
{
"id": 123,
"status": "PROCESSING",
"progress": 0,
"source_type": "FILE",
"original_filename": "source_speech.wav",
"target_voice_uuid": "550e8400-e29b-41d4-a716-446655440000",
"target_voice_name": "Professional Speaker Voice",
"created_at": "2024-01-15T10:30:00Z",
"task_id": "celery_task_uuid_here",
"user_id": "user_abc123def456"
}
```
## Job Management
### Get Conversion Status
Monitor the progress of voice conversion jobs.
```http theme={null}
GET /api/v1/voice/convert/{conversion_id}/status
X-API-Key: {api_key}
```
```python theme={null}
conversion_id = 123
response = requests.get(
f"https://api.audiopod.ai/api/v1/voice/convert/{conversion_id}/status",
headers={"X-API-Key": api_key}
)
if response.status_code == 200:
conversion_status = response.json()
print(f"Status: {conversion_status['status']}")
if conversion_status["status"] == "COMPLETED":
print("Voice conversion complete!")
print(f"Output path: {conversion_status.get('output_path')}")
print(f"Original filename: {conversion_status.get('original_filename')}")
```
**Response (Completed Job):**
```json theme={null}
{
"id": 123,
"status": "COMPLETED",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:33:45Z",
"completed_at": "2024-01-15T10:33:45Z",
"output_path": "outputs/converted/uuid/voice_converted.wav",
"error_message": null,
"task_id": "celery_task_uuid_here",
"source_path": "temp/uploads/uuid/source_speech.wav",
"original_filename": "source_speech.wav",
"conversion_metadata": {
"voice_uuid": "550e8400-e29b-41d4-a716-446655440000",
"voice_id": 456,
"voice_file_path": "voices/456/uuid/voice.wav",
"duration": 45.2
}
}
```
### List Conversion Jobs
Get all voice conversion jobs for the authenticated user.
```http theme={null}
GET /api/v1/voice/convert/jobs?status=COMPLETED&limit=10
X-API-Key:
```
```python theme={null}
response = requests.get(
"https://api.audiopod.ai/api/v1/voice/convert/jobs",
headers={"X-API-Key": api_key},
params={
"status": "COMPLETED", # Filter by status
"limit": 10,
"skip": 0
}
)
if response.status_code == 200:
jobs = response.json()
for job in jobs:
print(f"Job {job['id']}: {job['status']} - {job.get('original_filename', 'N/A')}")
voice_uuid = job.get('conversion_metadata', {}).get('voice_uuid', 'N/A')
print(f" Target voice UUID: {voice_uuid}")
```
### Download Converted Audio
```python theme={null}
def download_converted_audio(conversion_id, api_key):
"""Download converted audio from completed job"""
# Get conversion details
response = requests.get(
f"https://api.audiopod.ai/api/v1/voice/convert/{conversion_id}/status",
headers={"X-API-Key": api_key}
)
if response.status_code != 200:
return {"error": "Conversion not found"}
conversion = response.json()
if conversion["status"] != "COMPLETED":
return {"error": f"Conversion not completed. Status: {conversion['status']}"}
if not conversion.get("output_path"):
return {"error": "No output file available"}
# Note: The output_path is the S3 key. You would need to use your S3 service
# or a separate download endpoint to get the actual file.
return {
"success": True,
"output_path": conversion["output_path"],
"original_filename": conversion.get("original_filename"),
"target_voice_uuid": conversion.get("conversion_metadata", {}).get("voice_uuid"),
"status": "File ready for download"
}
# Usage
result = download_converted_audio(123, "your_api_key")
if result.get("success"):
print(f"Conversion completed: {result['output_path']}")
print(f"Original file: {result['original_filename']}")
print(f"Target voice UUID: {result['target_voice_uuid']}")
```
## Voice Profile Management
### Find Available Voices
Before converting, you can browse available voice profiles to use as targets.
```python theme={null}
# Get available voices for conversion
response = requests.get(
"https://api.audiopod.ai/api/v1/voice/voice-profiles",
headers={"X-API-Key": api_key},
params={
"voice_type": "CUSTOM", # Filter by voice type
"include_public": "true", # Include public voices
"limit": 20
}
)
if response.status_code == 200:
voices_data = response.json()
print("Available voices for conversion:")
for voice in voices_data["voices"]:
if voice["is_custom"] or voice.get("is_public"):
print(f"- {voice['name']} (UUID: {voice['voice_id']})")
print(f" Category: {voice['category']}")
print(f" Language: {voice['language']}")
print(f" Type: {'Custom' if voice['is_custom'] else 'Public'}")
print()
```
## Use Cases & Examples
### Podcast Voice Consistency
```python theme={null}
def convert_podcast_guest_to_host_voice(guest_audio, host_voice_uuid, api_key):
"""Convert guest audio to match host voice for consistency"""
print("Converting guest audio to match host voice...")
with open(guest_audio, "rb") as audio_file:
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voice-convert",
headers={"X-API-Key": api_key},
data={"voice_uuid": host_voice_uuid},
files={"file": audio_file}
)
if response.status_code != 200:
return {"error": "Failed to start conversion"}
job_data = response.json()
job_id = job_data["id"]
# Wait for completion
import time
while True:
status_response = requests.get(
f"https://api.audiopod.ai/api/v1/voice/convert/{job_id}/status",
headers={"X-API-Key": api_key}
)
job_status = status_response.json()
print(f"Progress: {job_status['progress']}%")
if job_status["status"] == "COMPLETED":
break
elif job_status["status"] == "FAILED":
return {"error": "Conversion failed"}
time.sleep(5)
# Download result
output_url = job_status["output_url"]
converted_filename = f"guest_as_host_{job_id}.wav"
audio_response = requests.get(output_url)
with open(converted_filename, "wb") as f:
f.write(audio_response.content)
return {
"success": True,
"job_id": job_id,
"output_file": converted_filename,
"original_duration": job_status["original_duration"],
"processing_time": job_status["processing_stats"]["total_time"]
}
# Usage
result = convert_podcast_guest_to_host_voice(
"guest_interview.wav",
"host_voice_uuid_here",
"your_api_key"
)
if result.get("success"):
print(f"Guest voice converted successfully!")
print(f"Output: {result['output_file']}")
print(f"Processing time: {result['processing_time']:.1f}s")
```
### Content Localization
```python theme={null}
def localize_content_voice(source_audio, target_locale_voices, api_key):
"""Convert content to different regional voice variations"""
localized_versions = {}
for locale, voice_uuid in target_locale_voices.items():
print(f"Creating {locale} version...")
with open(source_audio, "rb") as audio_file:
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voice-convert",
headers={"X-API-Key": api_key},
data={"voice_uuid": voice_uuid},
files={"file": audio_file}
)
if response.status_code == 200:
job_data = response.json()
localized_versions[locale] = {
"job_id": job_data["id"],
"status": "processing",
"target_voice": job_data["target_voice_name"]
}
# Monitor all jobs
completed_versions = {}
while len(completed_versions) < len(localized_versions):
for locale, job_info in localized_versions.items():
if locale in completed_versions:
continue
status_response = requests.get(
f"https://api.audiopod.ai/api/v1/voice/convert/{job_info['job_id']}/status",
headers={"X-API-Key": api_key}
)
job_status = status_response.json()
if job_status["status"] == "COMPLETED":
# Download localized version
audio_response = requests.get(job_status["output_url"])
filename = f"content_{locale}_{job_info['job_id']}.wav"
with open(filename, "wb") as f:
f.write(audio_response.content)
completed_versions[locale] = {
"filename": filename,
"target_voice": job_status["target_voice_name"],
"duration": job_status["converted_duration"]
}
print(f"Completed {locale} version: {filename}")
time.sleep(5)
return completed_versions
# Usage - create multiple regional versions
locale_voices = {
"US": "us_professional_voice_uuid",
"UK": "uk_professional_voice_uuid",
"AU": "au_professional_voice_uuid"
}
versions = localize_content_voice(
"original_content.wav",
locale_voices,
"your_api_key"
)
print("Localized versions created:")
for locale, info in versions.items():
print(f" {locale}: {info['filename']} ({info['duration']:.1f}s)")
```
### Voice Anonymization
```python theme={null}
def anonymize_voice_recording(source_audio, api_key):
"""Convert sensitive audio to an anonymous voice for privacy"""
# Use a generic public voice for anonymization
anonymous_voice_uuid = "public_generic_voice_uuid"
with open(source_audio, "rb") as audio_file:
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voice-convert",
headers={"X-API-Key": api_key},
data={"voice_uuid": anonymous_voice_uuid},
files={"file": audio_file}
)
job_id = response.json()["id"]
# Wait for completion
while True:
status_response = requests.get(
f"https://api.audiopod.ai/api/v1/voice/convert/{job_id}/status",
headers={"X-API-Key": api_key}
)
job_status = status_response.json()
if job_status["status"] == "COMPLETED":
break
time.sleep(3)
# Download anonymized audio
audio_response = requests.get(job_status["output_url"])
anonymized_filename = f"anonymized_{job_id}.wav"
with open(anonymized_filename, "wb") as f:
f.write(audio_response.content)
return {
"anonymized_file": anonymized_filename,
"original_content_preserved": True,
"voice_anonymized": True,
"duration": job_status["converted_duration"]
}
```
## Error Handling
**Causes:** - Target voice UUID doesn't exist - Voice is not accessible to user - Voice is not completed or ready
**Solutions:** - Verify voice UUID is correct - Ensure voice is completed and has audio file available - Use public voices or your own custom voices
**Causes:** - Source audio file has no speech content - Unsupported audio format - Audio file corrupted
**Solutions:** - Ensure audio contains clear speech - Use supported formats (WAV, MP3, M4A) - Verify file integrity
**Causes:** - Source audio quality too poor - Incompatible voice characteristics - Processing timeout
**Solutions:** - Improve source audio quality - Try different target voice - Use shorter audio segments
**Causes:** - Not enough credits for conversion duration
**Solutions:** - Purchase additional credits - Check credit requirements for audio duration
## Best Practices
### Audio Quality Guidelines
For optimal voice conversion results:
```python theme={null}
# Audio requirements for best results
audio_guidelines = {
"sample_rate": "16kHz or higher recommended",
"format": "WAV preferred, MP3/M4A acceptable",
"duration": "10 seconds to 10 minutes optimal",
"content": "Clear speech without background music",
"speaker": "Single speaker recommended",
"noise": "Minimal background noise"
}
# Pre-processing recommendations
preprocessing_tips = [
"Use noise reduction for noisy recordings",
"Normalize audio levels for consistent volume",
"Remove long silent periods to improve processing speed",
"Separate speakers if multiple voices present"
]
```
### Target Voice Selection
```python theme={null}
def choose_optimal_target_voice(source_characteristics, available_voices):
"""Choose the best target voice for conversion"""
recommendations = []
for voice in available_voices:
score = 0
# Language match
if voice["language"] == source_characteristics["language"]:
score += 30
# Gender match (if applicable)
if voice["gender"] == source_characteristics.get("gender"):
score += 20
# Age range compatibility
if voice["age_range"] == source_characteristics.get("age_range"):
score += 15
# Style appropriateness
if voice["style"] in source_characteristics.get("preferred_styles", []):
score += 25
# Quality indicators
if voice.get("usage_stats", {}).get("avg_rating", 0) >= 4.5:
score += 10
recommendations.append({
"voice": voice,
"compatibility_score": score,
"reasons": []
})
# Sort by compatibility score
recommendations.sort(key=lambda x: x["compatibility_score"], reverse=True)
return recommendations[:5] # Top 5 recommendations
```
## Pricing
Voice conversion is billed by audio duration:
| Path | Rate | Basis |
| - | - | - |
| **API wallet** (API key, USD) | **\$0.13 per minute** | Duration of the source audio |
| **Account credits** (subscription / JWT) | **990 credits per minute** | Duration of the source audio |
### Cost Examples — API wallet (per minute)
| Duration | USD Cost |
| - | - |
| 30 seconds | \$0.07 |
| 2 minutes | \$0.26 |
| 5 minutes | \$0.65 |
| 10 minutes | \$1.30 |
### Cost Optimization Tips
1. **Pre-process audio** to remove silence and optimize duration
2. **Batch similar conversions** using the same target voice
3. **Test with shorter clips** before converting long content
4. **Use high-quality source audio** to avoid re-processing
## Next Steps
Browse and manage available voice profiles for conversion.
Extract text from converted audio for verification.
# Voice Management
Source: https://docs.audiopod.ai/api-reference/voice-management
Manage custom voices, browse available voices, and control voice collections using AudioPod AI's Voice Management API.
## Overview
AudioPod AI's Voice Management API provides comprehensive tools for managing custom and pre-built voices. Browse available voices, organize custom voice collections, preview voices, and manage voice metadata for text-to-speech and voice cloning workflows.
### Key Features
* **Voice Browsing**: Explore available pre-built voices by category
* **Custom Voice Creation**: Create new voice models from audio samples
* **Custom Voice Management**: Organize and manage cloned voices
* **Voice Collections**: Group voices into organized collections
* **Voice Preview**: Generate preview samples to test voices
* **Metadata Management**: Update voice names, descriptions, and tags
* **Batch Operations**: Manage multiple voices efficiently
## Authentication
All endpoints require authentication. Use one of these methods:
* **API Key (Recommended)**: `X-API-Key: your_api_key` header
* **JWT Token**: `Authorization: Bearer your_jwt_token` (for session-based auth)
## Voice Discovery
### List Available Voices
Browse all available voices including pre-built and custom voices.
```http theme={null}
GET /api/v1/voice/voice-profiles?limit=50
X-API-Key: {api_key}
```
```python theme={null}
import requests
response = requests.get(
"https://api.audiopod.ai/api/v1/voice/voice-profiles",
headers={"X-API-Key": api_key},
params={
"limit": 50,
"skip": 0
}
)
if response.status_code == 200:
voices_data = response.json()
for voice in voices_data["voices"]:
print(f"Voice: {voice['name']} ({voice['voice_id']})")
print(f" Category: {voice['category']}")
print(f" Language: {voice['language']}")
print(f" Gender: {voice['gender']}")
print(f" Preview: {voice['preview_url']}")
print()
```
```bash theme={null}
curl -X GET "https://api.audiopod.ai/api/v1/voice/voice-profiles" \
-H "X-API-Key: your_api_key"
```
**Filter Parameters:**
* **`voice_type`**: `CUSTOM`, `STANDARD`
* **`is_public`**: `true`, `false`
* **`include_public`**: `true`, `false`
* **`skip`**: Number to skip for pagination
* **`limit`**: Number of results to return (max 50)
**Response:**
```json theme={null}
{
"voices": [
{
"voice_id": "voice_abc123def456",
"name": "Sarah Professional",
"category": "professional",
"language": "en",
"gender": "female",
"age_range": "adult",
"style": "conversational",
"description": "Clear, professional female voice perfect for business presentations",
"preview_url": "https://api.audiopod.ai/voice-previews/sarah_professional.mp3",
"is_custom": false,
"created_by": null,
"tags": ["business", "clear", "professional"],
"usage_stats": {
"total_generations": 15420,
"avg_rating": 4.7
},
"supported_features": {
"speed_control": true,
"pitch_control": true,
}
},
{
"voice_id": "voice_xyz789ghi012",
"name": "My Custom Voice",
"category": "custom",
"language": "en",
"gender": "male",
"age_range": "adult",
"style": "conversational",
"description": "Custom voice cloned from my recordings",
"preview_url": "https://api.audiopod.ai/voice-previews/custom_xyz789.mp3",
"is_custom": true,
"created_by": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2024-01-10T15:30:00Z",
"tags": ["personal", "english"],
"clone_job_id": 456,
"training_status": "completed",
"supported_features": {
"speed_control": true,
"pitch_control": true,
}
}
],
"total": 127,
"hasMore": true,
"filters_applied": {
"category": "professional",
"language": "en"
}
}
```
### Get Voice Details
Retrieve detailed information about a specific voice.
```http theme={null}
GET /api/v1/voice/voices/{voice_id}/status
X-API-Key: {api_key}
```
```python theme={null}
voice_id = 123 # Voice ID is actually an integer
response = requests.get(
f"https://api.audiopod.ai/api/v1/voice/voices/{voice_id}/status",
headers={"X-API-Key": api_key}
)
if response.status_code == 200:
voice = response.json()
print(f"Voice Name: {voice['name']}")
print(f"Description: {voice['description']}")
print(f"Language: {voice['language']}")
print(f"Category: {voice['category']}")
if voice['is_custom']:
print(f"Custom voice created: {voice['created_at']}")
print(f"Training status: {voice['training_status']}")
# Check supported features
features = voice['supported_features']
print(f"Speed control: {features['speed_control']}")
print(f"Pitch control: {features['pitch_control']}")
```
## Create Custom Voices
### Upload Single Audio Sample
Create a custom voice model from a single audio file.
```http theme={null}
POST /api/v1/voice/voice-profiles
X-API-Key: {api_key}
Content-Type: multipart/form-data
name: My Custom Voice
description: Professional voice for presentations
file: (audio file)
denoise: true
```
```python theme={null}
import requests
with open("voice_sample.wav", "rb") as audio_file:
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voice-profiles",
headers={"X-API-Key": api_key},
data={
"name": "My Custom Voice",
"description": "Professional voice for presentations",
"denoise": True
},
files={"file": audio_file}
)
if response.status_code == 200:
voice_data = response.json()
voice_id = voice_data["id"]
print(f"Voice created with ID: {voice_id}")
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/voice/voice-profiles" \
-H "X-API-Key: your_api_key" \
-F "name=My Custom Voice" \
-F "description=Professional voice for presentations" \
-F "denoise=true" \
-F "file=@voice_sample.wav"
```
**Response:**
```json theme={null}
{
"id": 123,
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Custom Voice",
"description": "Professional voice for presentations",
"voice_type": "CUSTOM",
"status": "PROCESSING",
"created_at": "2024-01-15T10:30:00Z",
"processed_at": null,
"is_public": false,
"file_path": "/voices/custom/voice_123.wav"
}
```
### Upload Multiple Audio Samples
Create a higher-quality voice model using multiple audio samples.
```http theme={null}
POST /api/v1/voice/voice-profiles/multi-sample
X-API-Key: {api_key}
Content-Type: multipart/form-data
name: High Quality Voice
description: Multi-sample voice for better quality
is_public: false
voice_files: (multiple audio files)
```
```python theme={null}
voice_files = [
"sample1.wav",
"sample2.wav",
"sample3.wav"
]
files = []
for i, file_path in enumerate(voice_files):
files.append(('voice_files', open(file_path, 'rb')))
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voice-profiles/multi-sample",
headers={"X-API-Key": api_key},
data={
"name": "High Quality Voice",
"description": "Multi-sample voice for better quality",
"is_public": False
},
files=files
)
# Close files
for _, file_obj in files:
file_obj.close()
```
### Audio Quality Requirements
For best voice creation results:
* **Duration**: 10-60 seconds of clean audio per sample
* **Format**: WAV, MP3, or M4A
* **Sample Rate**: 22kHz or higher
* **Quality**: Clear speech without background noise
* **Content**: Natural speech with varied intonation
* **Multiple Samples**: 2-5 samples recommended for higher quality
## Instant Voice Cloning (IVC)
Cloning is **instant** — a custom voice is ready to use in **seconds** from a
single short reference clip. Provide **5–30 seconds** of clean speech and the
returned voice can be used immediately for text-to-speech and voice conversion.
```http theme={null}
POST /api/v1/voice/voice-clone
X-API-Key: {api_key}
Content-Type: multipart/form-data
name: My Instant Clone
file: (5-30s reference audio)
```
```python theme={null}
import requests
with open("reference_5s.wav", "rb") as audio_file:
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/voice-clone",
headers={"X-API-Key": api_key},
data={"name": "My Instant Clone"},
files={"file": audio_file},
)
voice = response.json()
print(f"Voice ready: {voice['id']} — status {voice['status']}")
```
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/voice/voice-clone" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "name=My Instant Clone" \
-F "file=@reference_5s.wav"
```
For the highest fidelity, provide a clean 5–30s clip of a single speaker with
minimal background noise. Once created, use the voice with
[Text to Speech](/api-reference/text-to-speech) or
[Voice Changer](/api-reference/voice-changer).
## Design a Voice
Don't have a reference recording? **Design a voice from a text description.**
Describe the voice you want and get back audition-ready candidate previews, then
publish the one you like into a durable custom voice.
Voice Design is a **paid-tier feature** (Creator and above). Each generated
preview costs **250 credits**; publishing a chosen preview is free.
### Generate previews
`POST /api/v1/voice/design` describes a voice and returns short presigned
previews to audition.
```http theme={null}
POST /api/v1/voice/design
X-API-Key: {api_key}
Content-Type: application/json
{
"design_prompt": "A warm, middle-aged British narrator with a calm, unhurried delivery",
"preview_text": "This is a preview of the voice you just designed.",
"num_samples": 3
}
```
```python theme={null}
import requests
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/design",
headers={"X-API-Key": api_key, "Content-Type": "application/json"},
json={
"design_prompt": "A warm, middle-aged British narrator with a calm, unhurried delivery",
"preview_text": "This is a preview of the voice you just designed.",
"num_samples": 3,
},
)
data = response.json()
print(f"Charged {data['credits_charged']} credits")
for preview in data["previews"]:
print(f"{preview['preview_id']}: {preview['audio_url']}")
```
**Body:**
* `design_prompt` (required): Natural-language description of the voice (30–250 characters)
* `preview_text` (optional): The line the previews speak (20–300 characters)
* `num_samples` (optional): Number of candidate previews to generate, 1–3 (default 3). Each preview costs 250 credits
* `language` (optional): BCP-47 language code; auto-detected if omitted
**Response:**
```json theme={null}
{
"previews": [
{
"preview_id": "b1f2c3d4-....",
"audio_url": "https://presigned-url-to-preview.mp3",
"expires_at": "2026-07-09T12:00:00Z",
"preview_text": "This is a preview of the voice you just designed."
}
],
"credits_charged": 750
}
```
### Publish a preview
Turn a chosen preview into a permanent custom voice with
`POST /api/v1/voice/design/publish`. Publishing is free and the resulting voice
works everywhere a cloned voice does.
```http theme={null}
POST /api/v1/voice/design/publish
X-API-Key: {api_key}
Content-Type: application/json
{
"preview_id": "b1f2c3d4-....",
"name": "Calm British Narrator",
"description": "Designed voice for long-form narration"
}
```
```python theme={null}
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/design/publish",
headers={"X-API-Key": api_key, "Content-Type": "application/json"},
json={
"preview_id": "b1f2c3d4-....",
"name": "Calm British Narrator",
"description": "Designed voice for long-form narration",
},
)
voice = response.json()
print(f"Published voice: {voice['id']} ({voice['name']})")
```
**Body:**
* `preview_id` (required): The `preview_id` of the preview you want to keep
* `name` (required): Display name for the new voice (1–100 characters)
* `description` (optional): Notes about the voice
## Voice Preview
### Generate Voice Preview
Create a preview sample to test how a voice sounds.
```http theme={null}
GET /api/v1/voice/voice-profiles/{voice_identifier}/preview?audio_format=mp3
X-API-Key: {api_key}
```
```python theme={null}
voice_identifier = "123" # Voice ID or UUID
response = requests.get(
f"https://api.audiopod.ai/api/v1/voice/voice-profiles/{voice_identifier}/preview",
headers={"X-API-Key": api_key},
params={
"audio_format": "mp3",
"generate_if_missing": True
}
)
if response.status_code == 200:
preview_result = response.json()
print(f"Preview URL: {preview_result['audio_url']}")
print(f"Duration: {preview_result['duration']} seconds")
# Download preview audio
audio_response = requests.get(preview_result['audio_url'])
with open("voice_preview.mp3", "wb") as f:
f.write(audio_response.content)
print("Preview saved as voice_preview.mp3")
```
## Custom Voice Management
### List Custom Voices
Get all custom voices created by the authenticated user.
```http theme={null}
GET /api/v1/voice/voice-profiles?voice_type=CUSTOM&limit=25
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
"https://api.audiopod.ai/api/v1/voice/voice-profiles",
headers={"X-API-Key": api_key},
params={
"voice_type": "CUSTOM", # Filter for custom voices only
"limit": 25,
"skip": 0
}
)
if response.status_code == 200:
custom_voices = response.json()
for voice in custom_voices["voices"]:
print(f"Custom Voice: {voice['name']}")
print(f" Voice ID: {voice['voice_id']}")
print(f" Status: {voice['training_status']}")
print(f" Created: {voice['created_at']}")
print(f" Clone Job: {voice['clone_job_id']}")
print()
```
### Update Custom Voice
Update metadata and settings for a custom voice.
```http theme={null}
PATCH /api/v1/voice/voices/{voice_id}
X-API-Key: {api_key}
Content-Type: application/json
{
"name": "Updated Voice Name",
"description": "My updated custom voice description",
"is_public": false
}
```
```python theme={null}
voice_id = 123 # Voice ID is an integer
update_data = {
"name": "Updated Voice Name",
"description": "My updated custom voice description",
"is_public": False # Keep voice private
}
response = requests.patch(
f"https://api.audiopod.ai/api/v1/voice/voices/{voice_id}",
headers={
"X-API-Key": api_key,
"Content-Type": "application/json"
},
json=update_data
)
if response.status_code == 200:
updated_voice = response.json()
print(f"Voice updated successfully:")
print(f" Name: {updated_voice['name']}")
print(f" Description: {updated_voice['description']}")
print(f" Tags: {', '.join(updated_voice['tags'])}")
```
### Delete Custom Voice
Remove a custom voice and all associated data.
```http theme={null}
DELETE /api/v1/voice/voices/{voice_id}
X-API-Key: {api_key}
```
```python theme={null}
voice_id = 123 # Voice ID is an integer or an UUID
response = requests.delete(
f"https://api.audiopod.ai/api/v1/voice/voices/{voice_id}",
headers={"X-API-Key": api_key}
)
if response.status_code == 204:
print("Custom voice deleted successfully")
elif response.status_code == 404:
print("Voice not found or already deleted")
else:
print(f"Error deleting voice: {response.status_code}")
```
## Voice Collections
### Create Voice Collection
Organize voices into collections for better management.
```http theme={null}
POST /api/v1/voice/collections
X-API-Key: {api_key}
Content-Type: application/json
{
"name": "My Podcast Voices",
"description": "Voices for podcast production",
"voice_ids": ["voice_abc123", "voice_def456"],
"tags": ["podcast", "production"]
}
```
```python theme={null}
collection_data = {
"name": "My Podcast Voices",
"description": "Voices for podcast production",
"voice_ids": ["voice_abc123", "voice_def456"],
"tags": ["podcast", "production"],
"is_public": False
}
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/collections",
headers={
"X-API-Key": api_key,
"Content-Type": "application/json"
},
json=collection_data
)
if response.status_code == 201:
collection = response.json()
print(f"Collection created: {collection['name']}")
print(f"Collection ID: {collection['collection_id']}")
print(f"Contains {len(collection['voice_ids'])} voices")
```
### List Voice Collections
Get all voice collections for the authenticated user.
```http theme={null}
GET /api/v1/voice/collections?limit=25
X-API-Key: {api_key}
```
```python theme={null}
response = requests.get(
"https://api.audiopod.ai/api/v1/voice/collections",
headers={"X-API-Key": api_key},
params={"limit": 25, "skip": 0}
)
if response.status_code == 200:
collections = response.json()
for collection in collections["collections"]:
print(f"Collection: {collection['name']}")
print(f" Description: {collection['description']}")
print(f" Voices: {len(collection['voice_ids'])}")
print(f" Created: {collection['created_at']}")
print()
```
### Add Voices to Collection
Add voices to an existing collection.
```http theme={null}
POST /api/v1/voice/collections/{collection_id}/voices
X-API-Key: {api_key}
Content-Type: application/json
{
"voice_ids": ["voice_ghi789", "voice_jkl012"]
}
```
```python theme={null}
collection_id = "collection_abc123"
new_voices = {
"voice_ids": ["voice_ghi789", "voice_jkl012"]
}
response = requests.post(
f"https://api.audiopod.ai/api/v1/voice/collections/{collection_id}/voices",
headers={
"X-API-Key": api_key,
"Content-Type": "application/json"
},
json=new_voices
)
if response.status_code == 200:
updated_collection = response.json()
print(f"Added voices to collection")
print(f"Collection now has {len(updated_collection['voice_ids'])} voices")
```
## Error Handling
**Causes:** - Voice ID doesn't exist - Voice was deleted - Access denied to private voice
**Solutions:** - Verify voice ID is correct - Check if voice still exists - Ensure you have access permissions
**Causes:** - Invalid filter parameters - Malformed voice collection data - Invalid preview text
**Solutions:** - Check parameter values - Validate collection structure - Ensure preview text is not empty
**Causes:** - Trying to modify another user's custom voice - Accessing private voice without permission
**Solutions:** - Only modify your own custom voices - Use public voices or get proper access
**Causes:** - Not enough credits for voice preview generation
**Solutions:** - Purchase additional credits - Check credit requirements
Returned when creating a custom voice would exceed your plan's voice slots. Free/Basic plans include **3 custom voice slots**; Creator, Pro, and Studio plans have **unlimited** custom voices. Existing voices are never removed — deleting a voice frees a slot.
```json theme={null}
{
"detail": {
"code": "CUSTOM_VOICE_LIMIT_REACHED",
"message": "You've used 3 of 3 custom voices on the Basic plan. Delete a voice or upgrade for more.",
"feature": "custom_voices",
"current_plan": "basic",
"limit": 3,
"used": 3
}
}
```
**Solutions:** - Delete an unused custom voice to free a slot - Upgrade to a paid plan for unlimited custom voices
Check your current usage with `GET /api/v1/voice/voice-profiles/limits`:
```json theme={null}
{ "used": 3, "limit": 3, "unlimited": false, "plan": "basic" }
```
`limit` is `-1` and `unlimited` is `true` on plans without a cap.
## Use Cases & Examples
### Voice Selection Assistant
```python theme={null}
def find_best_voice_for_content(content_type, language, gender_preference, api_key):
"""Help users find the best voice for their content"""
# Define voice selection criteria based on content type
criteria_map = {
"podcast": {"category": "professional", "style": "conversational"},
"audiobook": {"category": "professional", "style": "narrative"},
"assistant": {"category": "professional", "style": "assistant"},
"character": {"category": "character", "style": "character"},
"casual": {"category": "casual", "style": "conversational"}
}
criteria = criteria_map.get(content_type, {"category": "professional"})
# Search for matching voices
params = {
"language": language,
"limit": 10
}
if gender_preference:
params["gender"] = gender_preference
if "category" in criteria:
params["category"] = criteria["category"]
if "style" in criteria:
params["style"] = criteria["style"]
response = requests.get(
"https://api.audiopod.ai/api/v1/voice/voice-profiles",
headers={"X-API-Key": api_key},
params=params
)
if response.status_code != 200:
return {"error": "Failed to fetch voices"}
voices_data = response.json()
voices = voices_data["voices"]
# Rank voices by suitability
ranked_voices = []
for voice in voices:
score = 0
# Prefer voices with higher ratings
if voice.get("usage_stats", {}).get("avg_rating"):
score += voice["usage_stats"]["avg_rating"] * 10
# Prefer voices with proven usage
if voice.get("usage_stats", {}).get("total_generations", 0) > 1000:
score += 20
# Prefer exact style matches
if voice.get("style") == criteria.get("style"):
score += 30
ranked_voices.append({
"voice": voice,
"suitability_score": score
})
# Sort by score
ranked_voices.sort(key=lambda x: x["suitability_score"], reverse=True)
return {
"content_type": content_type,
"language": language,
"criteria": criteria,
"recommended_voices": ranked_voices[:5], # Top 5 recommendations
"total_matches": len(voices)
}
# Usage
recommendations = find_best_voice_for_content(
content_type="podcast",
language="en",
gender_preference="female",
api_key="your_api_key"
)
print(f"Top recommendations for {recommendations['content_type']} in {recommendations['language']}:")
for i, rec in enumerate(recommendations["recommended_voices"], 1):
voice = rec["voice"]
print(f"{i}. {voice['name']} (Score: {rec['suitability_score']:.1f})")
print(f" Category: {voice['category']}, Style: {voice.get('style', 'N/A')}")
print(f" Rating: {voice.get('usage_stats', {}).get('avg_rating', 'N/A')}")
print()
```
### Voice Collection Manager
```python theme={null}
def organize_voices_by_project(voices_to_organize, api_key):
"""Organize voices into project-based collections"""
# Group voices by characteristics for project organization
voice_groups = {
"narrative_voices": [],
"character_voices": [],
"professional_voices": [],
"multilingual_voices": []
}
for voice_id in voices_to_organize:
# Get voice details
response = requests.get(
f"https://api.audiopod.ai/api/v1/voice/voices/{voice_id}",
headers={"X-API-Key": api_key}
)
if response.status_code == 200:
voice = response.json()
# Categorize voice
style = voice.get("style", "")
category = voice.get("category", "")
if style == "narrative" or "audiobook" in voice.get("tags", []):
voice_groups["narrative_voices"].append(voice_id)
elif category == "character" or style == "character":
voice_groups["character_voices"].append(voice_id)
elif category == "professional":
voice_groups["professional_voices"].append(voice_id)
# Check if multilingual
if voice.get("language") != "en" or "multilingual" in voice.get("tags", []):
voice_groups["multilingual_voices"].append(voice_id)
# Create collections for non-empty groups
created_collections = []
collection_configs = {
"narrative_voices": {
"name": "Narrative & Audiobook Voices",
"description": "Voices optimized for storytelling and long-form content"
},
"character_voices": {
"name": "Character & Creative Voices",
"description": "Unique voices for character work and creative projects"
},
"professional_voices": {
"name": "Professional & Business Voices",
"description": "Clear, professional voices for business content"
},
"multilingual_voices": {
"name": "International & Multilingual Voices",
"description": "Voices for international content and multiple languages"
}
}
for group_name, voice_ids in voice_groups.items():
if voice_ids: # Only create collection if there are voices
config = collection_configs[group_name]
collection_data = {
"name": config["name"],
"description": config["description"],
"voice_ids": voice_ids,
"tags": [group_name.replace("_", "-"), "auto-organized"]
}
response = requests.post(
"https://api.audiopod.ai/api/v1/voice/collections",
headers={
"X-API-Key": api_key,
"Content-Type": "application/json"
},
json=collection_data
)
if response.status_code == 201:
collection = response.json()
created_collections.append({
"collection_id": collection["collection_id"],
"name": collection["name"],
"voice_count": len(voice_ids),
"voices": voice_ids
})
return {
"organized": True,
"collections_created": len(created_collections),
"collections": created_collections,
"total_voices_organized": sum(len(group) for group in voice_groups.values())
}
# Usage
voice_list = ["voice_abc123", "voice_def456", "voice_ghi789", "voice_jkl012"]
organization_result = organize_voices_by_project(voice_list, "your_api_key")
print(f"Organized {organization_result['total_voices_organized']} voices into {organization_result['collections_created']} collections:")
for collection in organization_result["collections"]:
print(f" {collection['name']}: {collection['voice_count']} voices")
```
## Best Practices
### Voice Selection Guidelines
```python theme={null}
# Guidelines for choosing the right voice for different use cases
voice_selection_guide = {
"content_types": {
"podcast": {
"recommended_categories": ["professional", "casual"],
"preferred_styles": ["conversational"],
"features_needed": ["speed_control"],
"avoid": ["character", "synthetic"]
},
"audiobook": {
"recommended_categories": ["professional"],
"preferred_styles": ["narrative"],
"features_needed": ["pitch_control", "speed_control"],
"duration_considerations": "Choose voices with consistent quality for long content"
},
"e_learning": {
"recommended_categories": ["professional"],
"preferred_styles": ["assistant", "educational"],
"features_needed": ["speed_control"],
"clarity_priority": "High clarity and pronunciation accuracy required"
},
"marketing": {
"recommended_categories": ["professional", "character"],
"preferred_styles": ["promotional", "conversational"],
"brand_alignment": "Choose voice that matches brand personality"
}
},
"technical_considerations": {
"processing_speed": "Pre-built voices are faster than custom voices",
"customization": "Custom voices offer more personalization but require training time",
"quality": "Professional category voices generally have higher quality",
"cost": "Custom voices have training costs, pre-built voices are pay-per-use"
}
}
```
## Pricing
Voice management operations have different pricing structures:
| Operation | Cost | Description |
| - | - | - |
| Custom Voice Creation | Free | Create new voice model from audio samples |
| Voice Preview | Free | Generate preview samples (up to 50 words) |
| Voice Listing | Free | Browse available voices |
| Collection Management | Free | Create and manage voice collections |
| Custom Voice Updates | Free | Update custom voice metadata |
## Next Steps
Generate speech using both standard and custom voices.
Transform existing audio to use different voice characteristics.
# Audio AI Platform for Developers
Source: https://docs.audiopod.ai/index
AI-powered audio processing APIs. Audiobook production, stem separation, transcription, voice cloning, music generation, and more.
## Get Started in 5 Minutes
Create account, get API key, make your first call.
Prefer to click rather than type? Fork the **Getting Started** collection and hit
a real `200` in two requests — no account, no API key
([what's in it](/sdks/postman)).
[](https://god.gw.postman.com/run-collection/58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b%26entityType%3Dcollection%26workspaceId%3D8013472d-5897-47d1-bf5c-92683a027b35\&utm_source=audiopod-docs-index)
***
## Install SDK
```bash theme={null}
pip install audiopod
```
```bash theme={null}
npm install audiopod
```
No installation needed — use cURL directly with `X-API-Key` header.
***
## Hello World
```python theme={null}
from audiopod import AudioPod
client = AudioPod() # Uses AUDIOPOD_API_KEY env var
# Separate a song into vocals, drums, bass, guitar, piano, other
result = client.stems.separate(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="six"
)
print(result["download_urls"])
```
***
## What Can You Build?
Manuscript to ACX-ready audiobook via API. Upload PDF/EPUB, narrate, export — with completion webhooks.
Isolate vocals, drums, bass, guitar from any song — or target one
instrument from a 45-instrument catalog.
Clone any voice from 10 seconds of audio. Generate speech in that voice.
Convert audio/video to text. Speaker diarization, 50+ languages.
Generate music from text prompts. Any genre, any duration.
Remove background noise from audio. Multiple quality modes.
Identify and separate speakers from recordings.
150+ voices, 64+ languages. SSML support.
Transform voices with AI-powered style transfer.
***
## Pricing
Pay-as-you-go. No subscriptions required. Add funds to your wallet and use.
| Service | Rate |
| - | - |
| Stem Separation | \$0.10/min |
| Transcription | \$0.01/min |
| Voice Cloning/TTS | \$0.05 per 1,000 characters |
| Voice Conversion | \$0.13/min |
| Speech Translation | \$0.40/min |
| Music Generation | \$0.04/min |
| Speaker Separation | \$0.20/min |
| Karaoke Generation | \$0.25/min |
Complete pricing for all 14+ services
***
## Resources
Zero to API call in 5 minutes
Full Python SDK reference
Full Node.js SDK reference
Complete endpoint documentation
Check service health
Get help from our team
# Quick Start
Source: https://docs.audiopod.ai/quickstart
Go from zero to your first API call in under 5 minutes
**TL;DR:** Register → Verify email → Create API key → Add funds → Make API calls.
## Try it first — no signup
Want to hear a result before creating an account? The **sandbox** issues a
short-lived key (valid \~1 hour, small built-in credit balance) so you can make a
real text-to-speech call in two requests.
```bash theme={null}
# 1. Mint a sandbox key (no auth required)
SANDBOX_KEY=$(curl -s -X POST "https://api.audiopod.ai/api/v1/sandbox/key" | jq -r '.key')
# 2. Synthesize a short snippet (max 200 characters) — returns audio bytes
curl -s -X POST "https://api.audiopod.ai/api/v1/sandbox/tts" \
-H "Content-Type: application/json" \
-d "{\"key\": \"$SANDBOX_KEY\", \"text\": \"Hello from the AudioPod sandbox!\"}" \
--output sandbox.mp3
```
```python theme={null}
import requests
# 1. Mint a sandbox key (no auth required)
key = requests.post("https://api.audiopod.ai/api/v1/sandbox/key").json()["key"]
# 2. Synthesize a short snippet (max 200 characters)
audio = requests.post(
"https://api.audiopod.ai/api/v1/sandbox/tts",
json={"key": key, "text": "Hello from the AudioPod sandbox!"},
)
with open("sandbox.mp3", "wb") as f:
f.write(audio.content)
```
Rather not copy-paste? The same two requests are the first folder of our
**Getting Started** Postman collection. Fork it and run them — it stores the
sandbox key for you between the two calls.
[](https://god.gw.postman.com/run-collection/58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b%26entityType%3Dcollection%26workspaceId%3D8013472d-5897-47d1-bf5c-92683a027b35\&utm_source=audiopod-docs-quickstart)
The sandbox is rate-limited and capped at 200 characters per call — it's for a
first taste, not production. Ready for the full API? Continue below.
## Choose Your Path
Complete setup directly with cURL commands.
Login to AudioPod AI app to get your API key.
***
## Step 1: Create Your Account
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/initiate-registration" \
-H "Content-Type: application/json" \
-d '{
"email": "you@example.com",
"password": "YourSecurePassword123",
"full_name": "Your Name"
}'
```
✅ Check your email for a 6-digit verification code.
Create your free account
***
## Step 2: Verify Your Email
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/auth/verify-registration" \
-H "Content-Type: application/json" \
-d '{
"email": "you@example.com",
"verification_code": "123456"
}'
```
**Response includes your JWT token:**
```json theme={null}
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer"
}
```
Save the `access_token` — you need it for Step 3!
Enter the 6-digit code from your email on the verification page.
***
## Step 3: Create Your API Key
```bash theme={null}
# Use the access_token from Step 2
curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-first-key", "scopes": ["*"]}'
```
**Response:**
```json theme={null}
{
"api_key": "ap_xxxxxxxxxxxxxxxxxxxxxxxx"
}
```
**Save this key immediately!** It's only shown once.
1. Go to [API Keys](https://www.audiopod.ai/dashboard/account/api-keys)
2. Click **Create New API Key**
3. Copy and save your key
***
## Step 4: Set Up Your Environment
```bash theme={null}
# Add to your shell profile (~/.bashrc, ~/.zshrc)
export AUDIOPOD_API_KEY="ap_your_api_key_here"
```
Or create a `.env` file:
```bash theme={null}
AUDIOPOD_API_KEY=ap_your_api_key_here
```
***
## Step 5: Add Funds to Your Wallet
```bash theme={null}
# Get a Stripe payment link
curl -X POST "https://api.audiopod.ai/api/v1/api-wallet/topup/checkout" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount_cents": 500}'
```
Open the returned `url` in your browser to complete payment.
1. Go to [API Keys](https://www.audiopod.ai/dashboard/account/api-keys)
2. Click **Add Funds** in the Wallet Balance section
3. Choose amount (min \$1) and complete Stripe checkout
***
## Step 6: Make Your First API Call! 🎉
```bash theme={null}
pip install audiopod
```
```python theme={null}
from audiopod import AudioPod
client = AudioPod() # Uses AUDIOPOD_API_KEY env var
# Separate a song into stems
result = client.stems.separate(
url="https://youtube.com/watch?v=dQw4w9WgXcQ",
mode="six"
)
print("🎵 Stems ready!")
for stem, url in result["download_urls"].items():
print(f" {stem}: {url[:50]}...")
```
```bash theme={null}
npm install audiopod
```
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod();
const result = await client.stems.separate({
url: 'https://youtube.com/watch?v=dQw4w9WgXcQ',
mode: 'six'
});
console.log('🎵 Stems ready!');
Object.entries(result.download_urls).forEach(([stem, url]) => {
console.log(` ${stem}: ${url.substring(0, 50)}...`);
});
```
```bash theme={null}
# Separate audio into 6 stems
curl -X POST "https://api.audiopod.ai/api/v1/stem-extraction/api/extract" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-F "url=https://youtube.com/watch?v=dQw4w9WgXcQ" \
-F "mode=six"
# Check job status (replace JOB_ID)
curl -X GET "https://api.audiopod.ai/api/v1/stem-extraction/status/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
***
## Pricing
Pay only for what you use. No subscriptions required for API access.
| Service | Rate | \$10 Gets You |
| - | - | - |
| Stem Separation | \$0.10/min | 100 min |
| Transcription | \$0.01/min | 1,000 min |
| Voice Cloning/TTS | \$0.05 per 1,000 characters | \~200,000 characters |
| Voice Conversion | \$0.13/min | 77 min |
| Speech Translation | \$0.40/min | 25 min |
| Music Generation | \$0.04/min | 250 min |
| Speaker Separation | \$0.20/min | 50 min |
| Karaoke Generation | \$0.25/min | 40 min |
View complete pricing for all services
***
## Try More Features
```python theme={null}
# Convert speech to text
result = client.transcription.create(
url="https://youtube.com/watch?v=VIDEO_ID"
)
print(result["transcript"])
```
```python theme={null}
# Generate music from text
song = client.music.generate(
prompt="upbeat electronic dance music"
)
print(song["output_url"])
```
```python theme={null}
# Clone a voice from audio
voice = client.voice.clone(
file="./sample.wav",
name="My Voice"
)
# Generate speech with cloned voice
audio = client.tts.generate(
text="Hello world!",
voice_id=voice["id"]
)
```
```python theme={null}
# Remove background noise
clean = client.denoiser.denoise(
file="./noisy_audio.wav"
)
print(clean["output_url"])
```
***
## Manage Your Account (via CLI)
Once you have an API key, manage everything via cURL:
```bash theme={null}
# Check wallet balance
curl -s "https://api.audiopod.ai/api/v1/api-wallet/balance" \
-H "X-API-Key: $AUDIOPOD_API_KEY" | jq .
# List your API keys
curl -s "https://api.audiopod.ai/api/v1/auth/api-keys" \
-H "X-API-Key: $AUDIOPOD_API_KEY" | jq .
# Create another API key
curl -X POST "https://api.audiopod.ai/api/v1/auth/api-keys" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "production-key", "scopes": ["*"]}'
# View usage history
curl -s "https://api.audiopod.ai/api/v1/api-wallet/usage" \
-H "X-API-Key: $AUDIOPOD_API_KEY" | jq .
```
***
## Common Issues
Your API key is invalid or missing.
**Fix:** Check the `X-API-Key` header is set correctly:
```bash theme={null}
echo $AUDIOPOD_API_KEY # Should print your key
```
Your wallet is empty.
**Fix:** Add funds via CLI or dashboard:
```bash theme={null}
curl -X POST "https://api.audiopod.ai/api/v1/api-wallet/topup/checkout" \
-H "X-API-Key: $AUDIOPOD_API_KEY" \
-d '{"amount_cents": 500}'
```
Audio processing jobs can take time depending on file length.
**Fix:** Poll the status endpoint:
```bash theme={null}
curl "https://api.audiopod.ai/api/v1/stem-extraction/status/JOB_ID" \
-H "X-API-Key: $AUDIOPOD_API_KEY"
```
***
## Building a startup?
**AudioPod for Startups** gives eligible early-stage teams free Pro access plus
API credits to build on. Apply at
[audiopod.ai/startups](https://www.audiopod.ai/startups).
## Next Steps
Full SDK reference for Python & Node.js
Detailed endpoint documentation
Complete pricing & wallet management
Fork the collections instead of copy-pasting
# Claude Plugin
Source: https://docs.audiopod.ai/sdks/claude-plugin
Use AudioPod tools inside Claude Code — transcription, stems, noise removal, audio-to-MIDI and more through skills and slash commands.
**What is the AudioPod plugin?** It brings AudioPod tools into Claude Code. The plugin adds skills and slash commands that call the AudioPod [MCP server](/sdks/mcp), so you can transcribe, split stems, or turn a recording into MIDI without leaving your session. There are two plugins: `audiopod` (the main plugin) and `audiopod-studio` (the full toolkit).
## Install
In Claude Code, run:
```
/plugin marketplace add AudiopodAI/claude-plugin
/plugin install audiopod@audiopod
```
For the full toolkit, install `audiopod-studio` instead:
```
/plugin install audiopod-studio@audiopod
```
Install one of them, not both. Both register an MCP server named `audiopod`, so installing both would duplicate the tools.
The first time Claude uses an AudioPod tool it asks you to connect. Choose Connect, or run `/mcp`, select AudioPod and choose Authenticate. A browser window opens, you sign in to your AudioPod account, review what the connection can do, and choose Allow access. There's no API key to copy. You can disconnect at any time from your AudioPod account page.
### Using an API key instead (CI or headless)
[Create a key in your dashboard](https://audiopod.ai/dashboard/account/api-keys), then run:
```
claude mcp add --transport http audiopod https://mcp.audiopod.ai/core --header "X-API-Key: "
```
That is the endpoint for the main `audiopod` plugin. For `audiopod-studio`, use `https://mcp.audiopod.ai` instead. The key is added separately from the plugin, and the plugin's skills still work with it.
***
## The two plugins
| Plugin | What it covers | Endpoint |
| - | - | - |
| `audiopod` (AudioPod) | The main plugin: transcription (speaker labels, SRT/VTT/TXT), stem separation, noise removal, speaker separation, format conversion and audio-to-MIDI. | `https://mcp.audiopod.ai/core` |
| `audiopod-studio` (AudioPod Studio) | The full toolkit: everything in `audiopod` plus music, text to speech and voice tools. | `https://mcp.audiopod.ai` |
***
## Commands
| `audiopod` command | `audiopod-studio` command | Example |
| - | - | - |
| `/audiopod:transcribe` | `/audiopod-studio:transcribe` | `/audiopod:transcribe https://example.com/interview.mp3 srt` |
| `/audiopod:stems` | `/audiopod-studio:stems` | `/audiopod:stems https://example.com/song.mp3 4stem` |
| `/audiopod:midi` | `/audiopod-studio:midi` | `/audiopod:midi https://example.com/melody.mp3` |
| `/audiopod:status` | `/audiopod-studio:status` | `/audiopod:status transcribe` |
| not available | `/audiopod-studio:tts` | `/audiopod-studio:tts "Welcome to the show" British English` |
`/audiopod-studio:tts` exists only in the `audiopod-studio` plugin. You can also ask in plain language, like "remove the background noise from this clip", and the matching skill picks it up.
***
## How jobs work
AudioPod jobs run asynchronously. Claude starts the job, polls its status until it finishes, and hands you the output links. Output links expire after 1 hour. If one has expired, ask again (or run `/audiopod:status ` or `/audiopod-studio:status `, since job ids are unique per tool) for fresh links.
## Credits
Jobs use your AudioPod account's credits. See [pricing](https://audiopod.ai/pricing) for plans and credit details.
## Privacy
Audio you send is processed on AudioPod's servers under the [AudioPod privacy policy](https://audiopod.ai/privacy).
***
## Troubleshooting
| Symptom | Fix |
| - | - |
| Claude says AudioPod isn't connected or the connection expired | Run `/mcp`, select AudioPod and choose Authenticate. |
| `402` | Your account doesn't have enough credits for the job. Top up or upgrade on the [pricing page](https://audiopod.ai/pricing). |
| Tools not showing up | Run `/mcp` to check the connection status of the AudioPod server. |
| The browser sign-in page doesn't open (remote or headless machine) | Copy the URL Claude shows into a browser on any machine, or use the API-key option above. |
## Self-hosted MCP alternative
Prefer to wire up MCP yourself, or use a different client? See the [MCP Server](/sdks/mcp) page for the endpoint, auth, and client configs.
# CLI
Source: https://docs.audiopod.ai/sdks/cli
Generate music, voiceover, transcribe, separate stems, and clone voices from your terminal.
The `audiopod` CLI ships bundled with both SDKs. Same commands, same flags whether you `pip install audiopod` or `npm install -g audiopod`.
## Install
```bash theme={null}
pip install audiopod
```
Requires Python 3.8+.
```bash theme={null}
npm install -g audiopod
```
Requires Node 16+.
```bash theme={null}
pnpm add -g audiopod
```
***
## Authenticate
Get an API key from [your dashboard](https://www.audiopod.ai/dashboard/account/api-keys), then either:
```bash theme={null}
audiopod login
# Paste ap_* → stored at ~/.audiopod/config.json (mode 600)
```
…or set the environment variable:
```bash theme={null}
export AUDIOPOD_API_KEY=ap_YOUR_KEY
```
Per-call override:
```bash theme={null}
audiopod --api-key ap_OTHER_KEY music "test"
```
***
## Commands
### `audiopod music`
Generate a song, instrumental, rap, or vocal stem.
```bash theme={null}
audiopod music "lo-fi rainy 90 BPM" --duration 60 --out song.wav
audiopod music "intro for podcast about climate" --kind instrumental --out intro.wav
audiopod music "uplifting wedding ballad" --lyrics "We met..." --kind song
```
**Flags**
| Flag | Default | Description |
| - | - | - |
| `--kind` | `song` | One of `song`, `instrumental`, `rap`, `vocals`. |
| `--duration` | `60` | Seconds (10–600). |
| `--lyrics` | — | Optional lyric text for vocal modes. |
| `--out` | — | If set, the CLI waits for completion and downloads to this path. |
| `--timeout` | `600` | Max seconds to wait when `--out` is given. |
### `audiopod tts`
Synthesize speech.
```bash theme={null}
audiopod tts "Welcome to AudioPod." --voice 368 --out hello.mp3
audiopod tts "Bienvenue à AudioPod." --voice 368 --lang fr --out hello-fr.mp3
```
**Flags**
| Flag | Default | Description |
| - | - | - |
| `--voice` | `368` | AudioPod voice ID (integer) or public catalog name. |
| `--lang` | `en` | ISO language code. |
| `--speed` | `1.0` | Multiplier (0.25–4.0). |
| `--format` | `mp3` | `mp3`, `wav`, or `ogg`. |
| `--out` | — | If set, downloads on completion. |
### `audiopod transcribe`
Transcribe a local file or URL.
```bash theme={null}
audiopod transcribe meeting.mp3 --diarize --format srt > meeting.srt
audiopod transcribe https://example.com/podcast.mp3 --lang en --format json
```
| Flag | Description |
| - | - |
| `--lang` | Force a language (auto-detected if omitted). |
| `--diarize` | Emit speaker labels. |
| `--format` | `text`, `srt`, `vtt`, `json`. |
| `--out` | Write the transcript to a file. |
### `audiopod stems`
Separate audio into stems.
```bash theme={null}
audiopod stems track.wav --mode six
audiopod stems https://example.com/song.mp3 --mode two
```
| Flag | Description |
| - | - |
| `--mode` | `two` (vocal+instr), `four` (vocals/drums/bass/other), `six` (+guitar+piano), `sixteen`. |
### `audiopod clone`
Train a voice from a 5–30s reference clip.
```bash theme={null}
audiopod clone reference.wav --name "Narrator" --description "Calm narrator voice"
```
| Flag | Description |
| - | - |
| `--name` (required) | Display name for the cloned voice. |
| `--description` | Optional voice characteristics description. |
### `audiopod audiobook`
Produce a finished, ACX-compliant audiobook from a manuscript — upload, narrate, export, and (with `--out`) download the package in one command.
```bash theme={null}
audiopod audiobook book.epub --title "The Lighthouse Keeper" --author "A. P. Tester" --voice 387 --out lighthouse-acx.zip
audiopod audiobook --text "Once upon a time..." --title "A Short Tale" --voice 387
```
| Flag | Description |
| - | - |
| `` | Manuscript file (PDF/EPUB/DOCX/TXT). Omit and use `--text` to narrate raw text. |
| `--title` (required) | Audiobook title. |
| `--author` | Author name (written into the package metadata). |
| `--text` | Narrate this text instead of a file. |
| `--voice` | Narration voice ID (default 387). |
| `--out` | Download the ACX package ZIP to this path. |
| `--timeout` | Max seconds to wait for the full pipeline (default 1800). |
### `audiopod jobs`
Look up any async job by id.
```bash theme={null}
audiopod jobs job_abc123
audiopod jobs job_abc123 --json | jq .output_url
```
***
## Output
Human-readable by default; pass `--json` for scripting:
```bash theme={null}
audiopod tts "hi" --voice 368 --json | jq -r '.id'
```
***
## Exit codes
| Code | Meaning |
| - | - |
| 0 | Success |
| 1 | API or runtime error |
| 2 | Bad arguments / missing API key |
| 130 | Interrupted (Ctrl-C) |
***
## See also
Wire AudioPod into any agent.
Programmatic access from Python.
Programmatic access from Node.js / TypeScript.
Every developer surface in one page.
# MCP Server
Source: https://docs.audiopod.ai/sdks/mcp
Connect any agent to AudioPod via Model Context Protocol — music generation, TTS, voice cloning, transcription, and more from one endpoint.
**What is MCP?** [Model Context Protocol](https://modelcontextprotocol.io) is an open standard that lets AI assistants (Claude Desktop, Cursor, Continue, Cline, Goose, Codex…) call external tools through a single transport. AudioPod publishes a Streamable-HTTP MCP server that exposes ten audio AI tools.
## Endpoint
| Field | Value |
| - | - |
| URL | `https://mcp.audiopod.ai` |
| Transport | `streamable-http` (MCP 2025-06-18) |
| Auth | `X-API-Key: ap_*` (from your dashboard) **or** `Authorization: Bearer ` |
| Discovery | [`/.well-known/mcp/server-card.json`](https://audiopod.ai/.well-known/mcp/server-card.json) and [`/.well-known/oauth-protected-resource`](https://audiopod.ai/.well-known/oauth-protected-resource) |
Need a key? [Create one in your dashboard](https://www.audiopod.ai/dashboard/account/api-keys). Free tier credits unlock the full tool surface — no card required.
***
## Tools exposed
Schemas are returned by `tools/list` — these are the names and one-liners:
| Tool | Description | Required scope |
| - | - | - |
| `generate_music` | Songs, instrumentals, rap, vocal stems from a text prompt. The default music tool. | `music:generate` |
| `compose_music` | Score-first: write an editable score (no audio yet) for a song you want to shape before recording. 165 credits. | `music:generate` |
| `revise_music_score` | Change a score in plain words; the rest of the score is kept. 165 credits per revision. Available when enabled for the account. | `music:generate` |
| `record_music_score` | Record a score, optionally with a new style or new words for the same tune. \~16.5 credits per second of score. | `music:generate` |
| `get_music_score` | Read a score you own (key, tempo, sections, length). Free. | `audio:read` |
| `edit_music_score` | Deprecated alias of `record_music_score`. | `music:generate` |
| `text_to_speech` | TTS in 200+ languages with 200+ voices and custom clones — powered by AudioSonic. | `voice:synthesize` |
| `clone_voice` | Clone a voice from a 5–30s reference clip. | `voice:clone` |
| `separate_stems` | Up to 16 stems (vocals, drums, bass, guitar, piano, other…). Two-stem mode returns a vocals/instrumental (karaoke) split. | `stems:separate` |
| `transcribe_audio` | Word-level timestamps, speaker diarization, SRT/VTT/JSON. | `transcribe` |
| `denoise_audio` | Remove background noise while preserving voice character. | `audio:write` |
| `convert_media` | MP3 / WAV / FLAC / OGG / M4A / MP4 / MOV conversion. | `audio:write` |
| `separate_speakers` | Isolate each speaker into a separate track from multi-speaker audio. | `audio:write` |
| `change_voice` | Convert a recording to a different target voice (voice-to-voice). | `voice:synthesize` |
| `audio_to_midi` | Convert a recording of a melody, instrument or song into MIDI files (per-instrument plus one multitrack). Specialist piano and vocal engines need a Creator plan or above; see [pricing](https://audiopod.ai/pricing). | `audio:write` |
| `check_job_status` | Poll the status and result of a long-running job. | *none (free)* |
Scopes are enforced when authenticating with an API key. JWT-based auth implies full user privileges. Not sure which music tool to use? See [Choosing how to make music](/api-reference/music/choosing).
***
## Setup
### One-line install (recommended)
Most modern MCP hosts now register an HTTP MCP server with a single command — no manual JSON edit, no restart needed beyond the host's normal config reload. Pick the one that matches your tool:
```bash theme={null}
claude mcp add --transport http audiopod https://mcp.audiopod.ai \
--header "X-API-Key: ap_YOUR_KEY" --scope user
```
The `--scope user` flag installs at the user level so every Claude Code project picks up the server. Run `claude mcp list` to verify, then `/mcp` inside the TUI to see the eleven tools.
```bash theme={null}
codex mcp add audiopod --url https://mcp.audiopod.ai \
--header "X-API-Key: ap_YOUR_KEY"
```
Verify with `codex mcp list`. Inside the Codex TUI, `/mcp` shows the connected servers and their tools.
For any MCP client that only speaks stdio (older Claude Desktop builds, Continue, Cline before they ship native HTTP support, etc.), the [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) bridge spawns a stdio process that proxies to our HTTP endpoint:
```bash theme={null}
npx -y mcp-remote https://mcp.audiopod.ai \
--header "X-API-Key: ap_YOUR_KEY"
```
Wire it in as a stdio server in your client's config. Useful when manual JSON is too fiddly.
### Manual config (for desktop apps without a CLI)
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
```json theme={null}
{
"mcpServers": {
"audiopod": {
"url": "https://mcp.audiopod.ai",
"headers": {
"X-API-Key": "ap_YOUR_KEY"
}
}
}
}
```
Restart Claude Desktop. Try: *"Generate a 30-second lo-fi instrumental for studying."*
Edit `~/.cursor/mcp.json`:
```json theme={null}
{
"mcpServers": {
"audiopod": {
"url": "https://mcp.audiopod.ai",
"headers": { "X-API-Key": "ap_YOUR_KEY" }
}
}
}
```
Reload Cursor. Try: *"Use audiopod to generate a 60s upbeat intro track and download it."*
Edit `~/.continue/config.json`:
```json theme={null}
{
"experimental": {
"modelContextProtocolServers": [
{
"transport": {
"type": "streamable-http",
"url": "https://mcp.audiopod.ai",
"headers": { "X-API-Key": "ap_YOUR_KEY" }
}
}
]
}
}
```
Edit `~/.config/cline/cline_mcp_settings.json`:
```json theme={null}
{
"mcpServers": {
"audiopod": {
"url": "https://mcp.audiopod.ai",
"transport": "streamable-http",
"headers": { "X-API-Key": "ap_YOUR_KEY" }
}
}
}
```
Run `goose configure` and add a Streamable-HTTP extension:
```
Type: streamable-http
URL: https://mcp.audiopod.ai
Header: X-API-Key=ap_YOUR_KEY
```
Probe the server directly to confirm your key works:
```bash theme={null}
curl https://mcp.audiopod.ai \
-H "X-API-Key: ap_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'
```
***
## JSON-RPC examples
### Initialize
```json theme={null}
POST https://mcp.audiopod.ai
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"clientInfo": { "name": "my-agent", "version": "0.1.0" }
}
}
```
### List tools
```json theme={null}
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}
```
### Call a tool
```json theme={null}
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "generate_music",
"arguments": {
"prompt": "lo-fi rainy 90 BPM, soft pads",
"duration": 60,
"task_type": "text2music"
}
}
}
```
The response includes a `structuredContent.job_id` — long-running tools return a job id immediately. Poll the matching `/api/v1//jobs/` endpoint, or call `tools/call` again with the `jobs` lookup tool wired into your CLI.
***
## Auth flow
AudioPod publishes RFC 9728 OAuth Protected Resource Metadata so clients can discover how to authenticate:
```bash theme={null}
curl https://audiopod.ai/.well-known/oauth-protected-resource
```
Returns the issuer, supported scopes, and the bearer-token pattern. Most agent clients use a simple `X-API-Key: ap_*` header — get your key in the [dashboard](https://www.audiopod.ai/dashboard/account/api-keys).
***
## Troubleshooting
1. Re-check your config file path — most agents won't reload after edits without a restart.
2. Confirm the URL is exactly `https://mcp.audiopod.ai` (no trailing slash, no `/sse`).
3. Hit the discovery URL: `curl https://mcp.audiopod.ai` returns a small JSON descriptor when the proxy is healthy.
* Verify your key starts with `ap_`.
* Check the key's scopes — the [dashboard](https://www.audiopod.ai/dashboard/account/api-keys) shows the per-tool scopes granted.
* For wildcard access use scope `*`.
Music and stem extraction return job ids immediately and resolve over the next 30–180 seconds. Most clients display the partial result; if yours doesn't, fall back to the [REST API](/api-reference/music/jobs) to poll for completion.
***
## Next steps
Same eleven tools, terminal-first.
Single-pane overview of every developer surface.
Free credits, no card required.
Model Context Protocol reference.
# Node.js SDK
Source: https://docs.audiopod.ai/sdks/nodejs
Official Node.js SDK for AudioPod AI - Professional Audio Processing powered by AI
[](https://www.npmjs.com/package/audiopod)
[](https://nodejs.org/)
## Installation
```bash theme={null}
npm install audiopod
# or
yarn add audiopod
```
## Quick Start
```typescript theme={null}
import AudioPod from 'audiopod';
// Initialize client
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Separate audio into 6 stems
const result = await client.stems.separate({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'six'
});
// Download stems
for (const [stem, url] of Object.entries(result.download_urls)) {
console.log(`${stem}: ${url}`);
}
```
## Authentication
```typescript theme={null}
import AudioPod from 'audiopod';
// Method 1: Pass API key directly
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Method 2: Use environment variable (recommended)
// Set AUDIOPOD_API_KEY environment variable, then:
const client = new AudioPod();
```
***
## Stem Separation
Extract individual audio components from mixed recordings.
### Available Modes
| Mode | Stems | Output |
| - | - | - |
| `single` | 1 | Specified stem only (vocals, drums, bass, guitar, piano, other) |
| `two` | 2 | Vocals + Instrumental |
| `four` | 4 | Vocals, Drums, Bass, Other |
| `six` | 6 | Vocals, Drums, Bass, Guitar, Piano, Other |
| `producer` | 8 | + Kick, Snare, Hihat |
| `studio` | 12 | Full production toolkit |
| `mastering` | 16 | Maximum detail |
### Examples
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Six-stem separation from YouTube
const result = await client.stems.separate({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'six'
});
console.log(result.download_urls);
// From local file
const result = await client.stems.separate({
file: './song.mp3',
mode: 'four'
});
// Extract only vocals
const result = await client.stems.separate({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'single',
stem: 'vocals'
});
// Async job handling (for more control)
const job = await client.stems.extract({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'six'
});
console.log(`Job ID: ${job.id}`);
// Wait for completion
const result = await client.stems.waitForCompletion(job.id);
// Get available modes
const modes = await client.stems.modes();
modes.modes.forEach(m => {
console.log(`${m.mode}: ${m.description}`);
});
```
***
## Transcription
Convert audio to text with speaker diarization.
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// From URL
const job = await client.transcription.create({ url: 'https://...' });
const result = await client.transcription.waitForCompletion(job.id);
console.log(result.transcript);
// From file
const job = await client.transcription.create({ file: './audio.mp3' });
const result = await client.transcription.waitForCompletion(job.id);
```
***
## Voice Cloning & TTS
Create custom voices and generate speech.
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Clone a voice from audio sample
const voice = await client.voice.clone({
file: './sample.wav',
name: 'My Voice'
});
console.log(`Voice ID: ${voice.id}`);
// Generate speech with cloned voice
const audio = await client.voice.generate({
voiceId: voice.id,
text: 'Hello, this is my cloned voice speaking!'
});
```
***
## Music Generation (AudioMusic V2)
Generate AI music with AudioMusic V2 — text-to-music, covers, style transfer, audio analysis, and more.
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Simple mode — just describe what you want
const job = await client.music.simple({
query: 'a soft Bengali love song for a quiet evening',
});
const song = await client.music.waitForCompletion(job.id);
console.log(`Music URL: ${song.output_url}`);
// Full control with text2music
const advanced = await client.music.create({
prompt: 'upbeat pop, female vocals, catchy melody',
task: 'text2music',
lyrics: '[verse]\nSun is shining bright\n[chorus]\nDance the night away',
duration: 120,
inferenceSteps: 64,
guidanceScale: 7.0,
format: 'flac',
});
// Instrumental
const instrumental = await client.music.instrumental(
'chill jazz piano with saxophone', 90
);
// Cover / style transfer
const cover = await client.music.cover({
srcAudioUrl: 'https://example.com/song.mp3',
caption: 'jazz piano version with upright bass',
audioCoverStrength: 0.7,
});
// Analyze existing audio
const analysis = await client.music.analyze({
audioUrl: 'https://example.com/song.mp3',
});
console.log(`BPM: ${analysis.bpm}, Key: ${analysis.keyscale}`);
// Extract stems
const vocals = await client.music.extractStem({
srcAudioUrl: 'https://example.com/song.mp3',
trackName: 'vocals',
});
const completed = await client.music.waitForCompletion(vocals.id);
console.log(`Vocals URL: ${completed.output_url}`);
// Reference audio for style guidance
const ref = await client.music.reference({
referenceAudioUrl: 'https://example.com/reference.mp3',
caption: 'upbeat pop with similar warmth',
lyrics: '[verse]\nHello world',
});
```
***
## Audiobook
Turn a manuscript into a finished, ACX-compliant audiobook. Use `produce()` for the whole pipeline in one call, or drive each step. See the [Audiobook API reference](/api-reference/audiobook) for the full surface.
```typescript theme={null}
// One call: manuscript in, ACX package out
const result = await client.audiobook.produce({
title: 'The Lighthouse Keeper',
author: 'A. P. Tester',
file: 'book.epub', // PDF / EPUB / DOCX / TXT (or text: '...')
voiceId: 387,
});
console.log(result.download_url); // presigned ACX package ZIP
console.log(result.compliance_check.is_compliant);
// Or drive each step
const project = await client.audiobook.createProject({ title: 'My Book', author: 'Me' });
const up = await client.audiobook.uploadManuscript(project.id, 'book.epub');
await client.audiobook.parseManuscript(project.id, up.file_key, 'book.epub');
const voices = await client.audiobook.listVoices(); // available / recommended / custom
const estimate = await client.audiobook.costEstimate(project.id);
await client.audiobook.narrate(project.id, { voiceId: 387 }); // waits for all chapters
const exp = await client.audiobook.exportAndWait(project.id); // waits; runs ACX compliance check
const manifest = await client.audiobook.download(project.id, exp.job_id);
console.log(manifest.download_url);
```
Generate an original book from a prompt instead of uploading:
```typescript theme={null}
const project = await client.audiobook.createProject({ title: 'Bedtime Tales' });
await client.audiobook.generateBook(project.id, {
prompt: 'Three calm bedtime stories about the sea', chapterCount: 3, wordsPerChapter: 500,
});
await client.audiobook.narrate(project.id, { voiceId: 387 });
```
***
## Noise Reduction
Clean up audio by removing background noise.
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Denoise audio file
const clean = await client.denoiser.denoise({ file: './noisy.wav' });
console.log(`Clean audio: ${clean.outputUrl}`);
```
***
## Speaker Separation
Identify and separate multiple speakers.
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Diarize speakers
const speakers = await client.speaker.diarize({ url: 'https://...' });
console.log(`Found ${speakers.segments.length} speaker segments`);
```
***
## API Wallet
Check balance and manage billing.
```typescript theme={null}
import AudioPod from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Check balance
const balance = await client.wallet.balance();
console.log(`Balance: ${balance.balance_usd}`);
// Estimate cost before processing
const estimate = await client.wallet.estimate('stem_extraction', 180);
console.log(`Estimated cost: ${estimate.cost_usd}`);
// Get usage history
const usage = await client.wallet.usage();
usage.logs.forEach(log => {
console.log(`${log.service_type}: ${log.amount_usd}`);
});
```
***
## Error Handling
```typescript theme={null}
import AudioPod, { AuthenticationError, InsufficientBalanceError } from 'audiopod';
try {
const client = new AudioPod({ apiKey: 'ap_...' });
const result = await client.stems.separate({ url: '...', mode: 'six' });
} catch (error) {
if (error instanceof AuthenticationError) {
console.log('Invalid API key');
} else if (error instanceof InsufficientBalanceError) {
console.log(`Need more credits. Required: ${error.requiredCents} cents`);
} else {
console.log(`Error: ${error.message}`);
}
}
```
***
## TypeScript Support
Full TypeScript support with exported types:
```typescript theme={null}
import AudioPod, { StemExtractionJob, StemMode, WalletBalance } from 'audiopod';
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Fully typed responses
const job: StemExtractionJob = await client.stems.extract({
url: 'https://...',
mode: 'six' as StemMode
});
const balance: WalletBalance = await client.wallet.balance();
```
***
## Environment Variables
```bash theme={null}
export AUDIOPOD_API_KEY="ap_your_api_key"
```
```typescript theme={null}
// Client reads from env automatically
import AudioPod from 'audiopod';
const client = new AudioPod();
```
***
## Resources
View on npm
Source code
Generate your API key
Raw API documentation
# Official SDKs
Source: https://docs.audiopod.ai/sdks/overview
Get started quickly with AudioPod AI's official SDKs for Python and Node.js
## Ready-to-Use SDKs
AudioPod AI provides official SDKs that make it easy to integrate audio processing into your applications. Our SDKs handle authentication, request formatting, polling for job completion, and error handling automatically.
**pip install audiopod**
Full-featured Python SDK with type hints and async support.
**npm install audiopod**
TypeScript-ready SDK with promise-based API.
## Quick Comparison
| Feature | Python | Node.js |
| - | - | - |
| Package | `audiopod` | `audiopod` |
| Min Version | Python 3.8+ | Node 16+ |
| Type Support | Type hints | Full TypeScript |
| Async Support | Yes | Yes |
| File Upload | Yes | Yes |
| URL Processing | Yes | Yes |
| Auto-polling | Yes | Yes |
## Installation
```bash theme={null}
pip install audiopod
```
```bash theme={null}
npm install audiopod
# or
yarn add audiopod
```
## Quick Start
```python theme={null}
from audiopod import AudioPod
# Initialize client (uses AUDIOPOD_API_KEY env var)
client = AudioPod()
# Or pass API key directly
client = AudioPod(api_key="ap_your_api_key")
# Separate audio into stems
result = client.stems.separate(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="six"
)
# Download stems
for stem, url in result["download_urls"].items():
print(f"{stem}: {url}")
```
```typescript theme={null}
import AudioPod from 'audiopod';
// Initialize client (uses AUDIOPOD_API_KEY env var)
const client = new AudioPod();
// Or pass API key directly
const client = new AudioPod({ apiKey: 'ap_your_api_key' });
// Separate audio into stems
const result = await client.stems.separate({
url: 'https://youtube.com/watch?v=VIDEO_ID',
mode: 'six'
});
// Download stems
for (const [stem, url] of Object.entries(result.download_urls)) {
console.log(`${stem}: ${url}`);
}
```
## Available Services
Both SDKs provide access to the same set of services:
| Service | Python | Node.js | Description |
| - | - | - | - |
| Audiobook | `client.audiobook` | `client.audiobook` | Manuscript → ACX-ready audiobook |
| Stems | `client.stems` | `client.stems` | Audio stem separation |
| Transcription | `client.transcription` | `client.transcription` | Speech-to-text |
| Voice | `client.voice` | `client.voice` | Voice cloning and TTS |
| Music | `client.music` | `client.music` | AI music generation |
| Denoiser | `client.denoiser` | `client.denoiser` | Noise reduction |
| Speaker | `client.speaker` | `client.speaker` | Speaker diarization |
| Wallet | `client.wallet` | `client.wallet` | Balance and billing |
## Error Handling
Both SDKs provide typed exceptions for common error scenarios:
```python theme={null}
from audiopod import AudioPod, AuthenticationError, InsufficientBalanceError
try:
client = AudioPod(api_key="ap_...")
result = client.stems.separate(url="...", mode="six")
except AuthenticationError:
print("Invalid API key")
except InsufficientBalanceError as e:
print(f"Need ${e.required_cents / 100:.2f}")
```
```typescript theme={null}
import AudioPod, { AuthenticationError, InsufficientBalanceError } from 'audiopod';
try {
const client = new AudioPod({ apiKey: 'ap_...' });
const result = await client.stems.separate({ url: '...', mode: 'six' });
} catch (error) {
if (error instanceof AuthenticationError) {
console.log('Invalid API key');
} else if (error instanceof InsufficientBalanceError) {
console.log(`Need $${error.requiredCents / 100}`);
}
}
```
## Environment Variables
Both SDKs automatically read from the `AUDIOPOD_API_KEY` environment variable:
```bash theme={null}
export AUDIOPOD_API_KEY="ap_your_api_key"
```
## Next Steps
Complete Python SDK documentation
Complete Node.js SDK documentation
Raw API endpoint documentation
Generate your API key
# Postman Collections
Source: https://docs.audiopod.ai/sdks/postman
Fork the AudioPod AI API into Postman and make a real call without writing any code.
Two published collections live in the public **AudioPod** workspace on Postman.
Fork either one into your own workspace and it stays linked to ours, so you can
pull updates later.
## Start here — Getting Started
Eight requests in three folders. The first folder reaches a real `200` in two
requests **with no account and no API key**: it mints a short-lived sandbox key,
stores it in a collection variable, and synthesizes a snippet of speech. Folder 2
walks the submit-a-job / poll-the-status pattern the rest of the API uses, and
folder 3 covers your wallet balance and registering a webhook so you can stop
polling altogether.
[](https://god.gw.postman.com/run-collection/58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D58401244-916ddbda-1a70-4c07-8ccd-fe524703a84b%26entityType%3Dcollection%26workspaceId%3D8013472d-5897-47d1-bf5c-92683a027b35\&utm_source=audiopod-docs-postman)
## The full reference
Every endpoint we support — 299 requests across 13 folders: text to speech and
voice design, transcription, music, audiobook, podcast, reader, stem separation,
speaker separation, noise removal, audio to MIDI, webhooks, your wallet and your
API keys.
Use it as a browsable reference once you know the shape of a call.
[](https://god.gw.postman.com/run-collection/58401244-584f22a4-0bef-4783-92fc-4f62bc6df4c3?action=collection%2Ffork\&source=rip_markdown\&collection-url=entityId%3D58401244-584f22a4-0bef-4783-92fc-4f62bc6df4c3%26entityType%3Dcollection%26workspaceId%3D8013472d-5897-47d1-bf5c-92683a027b35\&utm_source=audiopod-docs-postman)
The reference collection is **regenerated from the live OpenAPI document** on a
schedule and pushed to the same collection id, so a fork tracks the real API
instead of drifting away from it. The Getting Started collection is
hand-maintained and deliberately small.
## Authenticating your fork
Every authenticated request sends `X-API-Key: {{apiKey}}`. You have two ways to
fill that in.
1. [Create an API key](https://www.audiopod.ai/dashboard/account/api-keys) in your dashboard.
2. Open the forked collection → **Variables** tab.
3. Paste the key into the **current value** of `apiKey`, and leave `baseUrl`
at `https://api.audiopod.ai`.
Keep the key in *current value*, not *initial value* — initial values travel
with the collection when you share it.
The workspace also publishes an **AudioPod Production** environment holding
`baseUrl` and an empty `apiKey`. Fork it from the workspace, select it in the
environment picker, and set `apiKey` there instead. This is the better option
if you switch between several keys.
Folder 1 of Getting Started needs neither — the sandbox is keyless. If you run an
authenticated request with `apiKey` still blank, the collection's pre-request
script stops it with an explanatory message rather than sending an unauthorized
call.
## Browse without forking
Eight requests, keyless first call
The full 299-request reference
## Next steps
The same flow in cURL, Python and Node.js
Create, scope and rotate keys
One subscription, every job type — stop polling
# Python SDK
Source: https://docs.audiopod.ai/sdks/python
Official Python SDK for AudioPod AI - Professional Audio Processing powered by AI
[](https://pypi.org/project/audiopod/)
[](https://www.python.org/downloads/)
## Installation
```bash theme={null}
pip install audiopod
```
## Quick Start
```python theme={null}
from audiopod import AudioPod
# Initialize client
client = AudioPod(api_key="ap_your_api_key")
# Separate audio into 6 stems
result = client.stems.separate(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="six"
)
# Download stems
for stem, url in result["download_urls"].items():
print(f"{stem}: {url}")
```
## Authentication
```python theme={null}
from audiopod import AudioPod
# or: from audiopod import Client
# Method 1: Pass API key directly
client = AudioPod(api_key="ap_your_api_key")
# Method 2: Use environment variable (recommended)
# Set AUDIOPOD_API_KEY environment variable, then:
client = AudioPod()
```
***
## Stem Separation
Extract individual audio components from mixed recordings.
### Available Modes
| Mode | Stems | Output |
| - | - | - |
| `single` | 1 | Specified stem only (vocals, drums, bass, guitar, piano, other) |
| `two` | 2 | Vocals + Instrumental |
| `four` | 4 | Vocals, Drums, Bass, Other |
| `six` | 6 | Vocals, Drums, Bass, Guitar, Piano, Other |
| `producer` | 8 | + Kick, Snare, Hihat |
| `studio` | 12 | Full production toolkit |
| `mastering` | 16 | Maximum detail |
### Examples
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Six-stem separation from YouTube (waits for completion by default)
result = client.stems.separate(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="six"
)
print(result["download_urls"])
# From local file
result = client.stems.separate(
file="./song.mp3",
mode="four"
)
# Extract only vocals
result = client.stems.separate(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="single",
stem="vocals"
)
# Async job handling (for more control)
job = client.stems.extract(
url="https://youtube.com/watch?v=VIDEO_ID",
mode="six"
)
print(f"Job ID: {job['id']}")
# Check status
status = client.stems.status(job["id"])
print(f"Status: {status['status']}")
# Wait for completion
result = client.stems.wait_for_completion(job["id"])
# Get available modes
modes = client.stems.modes()
for m in modes["modes"]:
print(f"{m['mode']}: {m['description']}")
```
***
## Transcription
Convert audio to text with speaker diarization.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# From URL
job = client.transcription.create(url="https://...")
result = client.transcription.wait_for_completion(job["id"])
print(result["transcript"])
# From file
job = client.transcription.create(file="./audio.mp3")
result = client.transcription.wait_for_completion(job["id"])
```
***
## Voice Cloning and TTS
Create custom voices and generate speech.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Clone a voice from audio sample
voice = client.voice.clone(
file="./sample.wav",
name="My Voice"
)
print(f"Voice ID: {voice['id']}")
```
***
## Music Generation (AudioMusic V2)
Generate AI music with AudioMusic V2 — text-to-music, covers, style transfer, audio analysis, and more.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Simple mode — just describe what you want
song = client.music.simple(
query="a soft Bengali love song for a quiet evening",
wait_for_completion=True
)
print(f"Music URL: {song['output_url']}")
# Full control with text2music
song = client.music.generate(
prompt="upbeat pop, female vocals, catchy melody",
task="text2music",
lyrics="[verse]\nSun is shining bright today\n[chorus]\nLet's dance the night away",
duration=120,
inference_steps=64,
guidance_scale=7.0,
format="flac",
wait_for_completion=True
)
# Instrumental
instrumental = client.music.instrumental(
prompt="chill jazz piano with saxophone",
duration=90,
wait_for_completion=True
)
# Cover / style transfer
cover = client.music.cover(
src_audio_url="https://example.com/song.mp3",
caption="jazz piano version with upright bass",
audio_cover_strength=0.7,
wait_for_completion=True
)
# Analyze existing audio
analysis = client.music.analyze(
audio_url="https://example.com/song.mp3"
)
print(f"BPM: {analysis['bpm']}, Key: {analysis['keyscale']}")
# Extract stems
vocals = client.music.extract_stem(
src_audio_url="https://example.com/song.mp3",
track_name="vocals",
wait_for_completion=True
)
print(f"Vocals URL: {vocals['output_url']}")
# Reference audio for style guidance
ref_song = client.music.reference(
reference_audio_url="https://example.com/reference.mp3",
caption="upbeat pop with similar warmth",
lyrics="[verse]\nHello world",
wait_for_completion=True
)
```
***
## Audiobook
Turn a manuscript into a finished, ACX-compliant audiobook. Use `produce()` for the whole pipeline in one call, or drive each step. See the [Audiobook API reference](/api-reference/audiobook) for the full surface.
```python theme={null}
# One call: manuscript in, ACX package out
result = client.audiobook.produce(
title="The Lighthouse Keeper",
author="A. P. Tester",
file="book.epub", # PDF / EPUB / DOCX / TXT (or text="...")
voice_id=387,
)
print(result["download_url"]) # presigned ACX package ZIP
print(result["compliance_check"]["is_compliant"])
# Or drive each step
project = client.audiobook.create_project(title="My Book", author="Me")
up = client.audiobook.upload_manuscript(project["id"], file="book.epub")
client.audiobook.parse_manuscript(project["id"], up["file_key"], "book.epub")
voices = client.audiobook.list_voices() # available / recommended / custom
estimate = client.audiobook.cost_estimate(project["id"])
client.audiobook.narrate(project["id"], voice_id=387) # waits for all chapters
export = client.audiobook.export(project["id"]) # waits; runs ACX compliance check
manifest = client.audiobook.download(project["id"], export["job_id"])
print(manifest["download_url"])
```
Generate an original book from a prompt instead of uploading:
```python theme={null}
project = client.audiobook.create_project(title="Bedtime Tales")
client.audiobook.generate_book(project["id"], prompt="Three calm bedtime stories about the sea",
chapter_count=3, words_per_chapter=500)
client.audiobook.narrate(project["id"], voice_id=387)
```
***
## Noise Reduction
Clean up audio by removing background noise.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Denoise audio file
clean = client.denoiser.denoise(file="./noisy.wav")
print(f"Clean audio: {clean['output_url']}")
```
***
## Speaker Separation
Identify and separate multiple speakers.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Diarize speakers
speakers = client.speaker.diarize(url="https://...")
print(f"Found {len(speakers['segments'])} speaker segments")
```
***
## API Wallet
Check balance and manage billing.
```python theme={null}
from audiopod import AudioPod
client = AudioPod(api_key="ap_your_api_key")
# Check balance
balance = client.wallet.balance()
print(f"Balance: {balance['balance_usd']}")
# Estimate cost before processing
estimate = client.wallet.estimate("stem_extraction", duration_seconds=180)
print(f"Estimated cost: {estimate['cost_usd']}")
# Get usage history
usage = client.wallet.usage()
for log in usage["logs"]:
print(f"{log['service_type']}: {log['amount_usd']}")
```
***
## Error Handling
```python theme={null}
from audiopod import AudioPod, AuthenticationError, InsufficientBalanceError
try:
client = AudioPod(api_key="ap_...")
result = client.stems.separate(url="...", mode="six")
except AuthenticationError:
print("Invalid API key")
except InsufficientBalanceError as e:
print(f"Need more credits. Required: {e.required_cents} cents")
except ValueError as e:
print(f"Invalid input: {e}")
```
***
## Environment Variables
```bash theme={null}
export AUDIOPOD_API_KEY="ap_your_api_key"
```
```python theme={null}
# Client reads from env automatically
from audiopod import AudioPod
client = AudioPod()
```
***
## Resources
View on PyPI
Source code
Generate your API key
Raw API documentation