Generate speech programmatically using an API key tied to your account. API requests use the exact same credits and the exact same admin-configured access window as the dashboard.
On this page
Authentication & API keys Access-time restrictions Endpoints Example requests Error responses Rate limits & other restrictionsEvery request to the API must include your API key in the X-API-Key header. This is separate from the session token your browser uses when you're logged into the dashboard.
X-API-Key header on every request.DELETE /api/keys) before creating a replacement. This is enforced by the backend, so it can't be worked around by calling the endpoint directly.
Delete your key at any time from the dashboard's API tab, or by calling DELETE /api/keys with your normal dashboard session. Once deleted, that key immediately stops working for all API requests — you can then generate exactly one new key.
Your account has an admin-configured allowed usage window, defined in Indian Standard Time (IST, UTC+5:30). This window applies identically to the dashboard and to the API — your API key inherits its permissions and time restrictions directly from your account, so there's no separate or conflicting restriction to configure. Requests made outside your allowed window are rejected by the backend, not just blocked in the UI.
If you're unsure of your current window, check the credits/status line on your dashboard, or ask an administrator.
Base URL: the same origin your dashboard is served from, e.g. http://localhost:4000.
| Method | Path | Description |
|---|---|---|
POST | /api/v1/generate | Submit text for speech generation. Returns immediately with a request ID; generation happens asynchronously. |
GET | /api/v1/status/:requestId | Check the status of a generation request. |
GET | /api/v1/audio/:requestId | Download the generated audio once status is generated. This is also the moment credits are charged. |
Headers:
| Header | Value |
|---|---|
X-API-Key | Your API key (required) |
Content-Type | application/json |
Body parameters:
| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Text to convert to speech. Up to 5,000 characters. |
voiceId | string | Yes | The voice to use. |
clientRequestId | string | No | Optional idempotency key. Resubmitting the same value returns the existing request instead of creating a duplicate/double-charging. |
curl -X POST http://localhost:4000/api/v1/generate \
-H "X-API-Key: sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"text": "Hello from the API!",
"voiceId": "your-voice-id"
}'
curl http://localhost:4000/api/v1/status/REQUEST_ID \
-H "X-API-Key: sk_live_your_key_here"
curl http://localhost:4000/api/v1/audio/REQUEST_ID \
-H "X-API-Key: sk_live_your_key_here" \
-o output.mp3
import time
import requests
API_BASE = "http://localhost:4000"
API_KEY = "sk_live_your_key_here"
headers = {"X-API-Key": API_KEY}
# 1. Submit the generation request
resp = requests.post(
f"{API_BASE}/api/v1/generate",
headers=headers,
json={"text": "Hello from the API!", "voiceId": "your-voice-id"},
)
data = resp.json()
if not data.get("success"):
raise RuntimeError(data.get("error"))
request_id = data["request"]["requestId"]
# 2. Poll until generation finishes
while True:
status_resp = requests.get(f"{API_BASE}/api/v1/status/{request_id}", headers=headers)
status_data = status_resp.json()
job_status = status_data["request"]["jobStatus"]
if job_status == "generated":
break
if job_status == "failed":
raise RuntimeError(status_data["request"].get("errorMessage"))
time.sleep(1)
# 3. Download the audio (this is also when credits are charged)
audio_resp = requests.get(f"{API_BASE}/api/v1/audio/{request_id}", headers=headers)
with open("output.mp3", "wb") as f:
f.write(audio_resp.content)
const API_BASE = "http://localhost:4000";
const API_KEY = "sk_live_your_key_here";
const headers = { "X-API-Key": API_KEY };
async function generateSpeech(text, voiceId) {
// 1. Submit the generation request
const genRes = await fetch(`${API_BASE}/api/v1/generate`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ text, voiceId }),
});
const genData = await genRes.json();
if (!genData.success) throw new Error(genData.error);
const requestId = genData.request.requestId;
// 2. Poll until generation finishes
let jobStatus = genData.request.jobStatus;
while (jobStatus !== "generated") {
await new Promise((r) => setTimeout(r, 1000));
const statusRes = await fetch(`${API_BASE}/api/v1/status/${requestId}`, { headers });
const statusData = await statusRes.json();
jobStatus = statusData.request.jobStatus;
if (jobStatus === "failed") throw new Error(statusData.request.errorMessage);
}
// 3. Download the audio (this is also when credits are charged)
const audioRes = await fetch(`${API_BASE}/api/v1/audio/${requestId}`, { headers });
return Buffer.from(await audioRes.arrayBuffer());
}
generateSpeech("Hello from the API!", "your-voice-id")
.then((audioBuffer) => require("fs").writeFileSync("output.mp3", audioBuffer))
.catch(console.error);
{
"success": true,
"request": {
"requestId": "b3e1c2a0-...",
"userId": "6f2a...",
"voiceId": "your-voice-id",
"characterCount": 20,
"jobStatus": "queued",
"audioUrl": null,
"createdAt": "2026-09-08T09:15:00.000Z"
},
"backendStatus": {
"isRefreshing": false,
"secondsRemaining": 0,
"canTakeJob": true,
"workerOnline": true,
"workersAvailable": 1,
"totalRemaining": 10000,
"message": null
}
}
All API errors use the same envelope: { "success": false, "error": "..." }, with additional fields where noted below.
{
"success": false,
"error": "Missing API key. Provide it via the X-API-Key header."
}
{
"success": false,
"error": "Invalid API key."
}
Returned with HTTP status 403 when a request arrives outside your account's allowed IST access window:
{
"success": false,
"error": "API access is not authorized at this time.",
"allowed_time": "9:00 AM to 6:00 PM IST"
}
| Status | Meaning |
|---|---|
400 | Invalid request body (missing/too-long text, invalid voice ID, etc). |
402 | Insufficient credits for the requested text length. |
403 | Account disabled, or outside the allowed access window. |
404 | Request ID not found, doesn't belong to your account, or audio isn't ready yet. |
429 | Rate limit exceeded — see below. |
/api/v1/generate requests per minute per account (shared with the dashboard's own generation limit)./api/v1/audio/:requestId — never merely on submission.backendStatus object describing your own generation worker — isRefreshing / secondsRemaining (your worker is refreshing), canTakeJob, workerOnline, workersAvailable, totalRemaining and a human-readable message. A request submitted while your worker is offline or refreshing may be rejected or parked until the worker is available.