Async Jobs
All batch TTS requests are processed asynchronously. Submit a job, get a job ID, then poll for the audio result or receive it via webhook.
When to Use Async
POST /v1/audio/speech returns 200 with binary audio by default (sync). When you provide a webhook_url parameter, it returns 202 Accepted with a job ID for async processing. Use GET /v1/jobs/{jobId} to poll, or wait for the webhook callback.
How It Works
Submit a batch request
POST /v1/audio/speech with a webhook_url parameter returns 202 immediately with a job ID like job_a1b2c3d4e5f6g7h8. Without webhook_url, it returns 200 with binary audio (sync).
Job processes on GPU
The job enters the RunPod queue. Status progresses: queued -> processing -> completed or failed.
Retrieve the audio
Poll GET /v1/jobs/{jobId} — returns binary audio when complete. Optionally add webhook_url to receive the result via POST.
Submitting a Job
curl -X POST "https://api.murmr.dev/v1/audio/speech" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "A long article to convert to speech...",
"voice": "voice_abc123",
"response_format": "mp3"
}'
# Returns 202:
# {"id": "job_a1b2c3d4e5f6g7h8", "status": "queued", "created_at": "..."}Optionally add webhook_url to receive the completed audio via POST to your server. See Webhook Delivery below.
Submit Response
{
"id": "job_a1b2c3d4e5f6g7h8",
"status": "queued",
"created_at": "2026-02-13T10:30:00.000Z"
}| Parameter | Type | Description |
|---|---|---|
idrequired | string | Unique job identifier (job_ + 16 hex chars). Use this to poll for status. |
statusrequired | string | Always "queued" on submission. |
created_atrequired | string | ISO 8601 timestamp of when the job was submitted. |
Polling for Results
/v1/jobs/{jobId}Returns current job status. Requires the same API key used to submit the job. Only the job owner can access their jobs.
Response varies by status
- In progress: Returns JSON with
statusfield - Completed: Returns binary audio (Content-Type: audio/*) — save directly to file
- Failed: Returns JSON with
errorfield - Expired: Returns
410 Goneif audio has been purged
In-Progress Response
{
"id": "job_a1b2c3d4e5f6g7h8",
"status": "processing",
"created_at": "2026-02-13T10:30:00.000Z"
}Completed Response
Binary audio data. Check the Content-Type header to determine the format. Response headers include X-Audio-Duration-Ms and X-Total-Time-Ms.
Failed Response
{
"id": "job_a1b2c3d4e5f6g7h8",
"status": "failed",
"error": "Voice not found: voice_invalid",
"created_at": "2026-02-13T10:30:00.000Z"
}Polling Example
import { MurmrClient, MurmrError } from '@murmr/sdk';
import { writeFileSync } from 'fs';
const client = new MurmrClient({ apiKey: process.env.MURMR_API_KEY! });
// Option 1: Manual polling
const status = await client.jobs.get('job_a1b2c3d4e5f6g7h8');
if (status.status === 'completed' && status.audio_base64) {
const audio = Buffer.from(status.audio_base64, 'base64');
writeFileSync(`output.${status.response_format || 'wav'}`, audio);
}
// Option 2: Wait for completion (polls automatically)
try {
const result = await client.jobs.waitForCompletion('job_a1b2c3d4e5f6g7h8', {
pollIntervalMs: 3000,
timeoutMs: 900_000,
onPoll: (status) => console.log(`Status: ${status.status}`),
});
if (result.audio_base64) {
writeFileSync('output.wav', Buffer.from(result.audio_base64, 'base64'));
}
} catch (error) {
if (error instanceof MurmrError && error.code === 'JOB_FAILED') {
console.error('Job failed:', error.message);
}
}
// Option 3: Submit and wait in one call
const final = await client.speech.createAndWait({
input: 'Submit and wait in one call.',
voice: 'voice_abc123',
response_format: 'mp3',
});Polling Best Practice
Poll every 3-5 seconds. Job metadata expires after 24 hours. Audio data on the backend may be purged sooner — retrieve promptly after completion to avoid a 410 Gone response.
Webhook Delivery
Add webhook_url to your request to receive the audio via POST when the job completes. Polling still works as a fallback.
curl -X POST "https://api.murmr.dev/v1/audio/speech" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Text to synthesize...",
"voice": "voice_abc123",
"response_format": "mp3",
"webhook_url": "https://your-server.com/webhooks/tts"
}'Success Payload
{
"id": "job_a1b2c3d4e5f6g7h8",
"status": "completed",
"audio": "<base64-encoded audio>",
"content_type": "audio/mpeg",
"response_format": "mp3",
"duration_ms": 4520,
"total_time_ms": 3200
}Failure Payload
{
"id": "job_a1b2c3d4e5f6g7h8",
"status": "failed",
"error": "Voice not found: voice_invalid"
}Webhook Handler Example
import base64
from flask import Flask, request
app = Flask(__name__)
@app.route("/webhooks/tts", methods=["POST"])
def handle_tts_webhook():
payload = request.json
if payload["status"] == "completed":
audio_bytes = base64.b64decode(payload["audio"])
filename = f"{payload['id']}.{payload['response_format']}"
with open(filename, "wb") as f:
f.write(audio_bytes)
print(f"Saved {filename} ({payload['duration_ms']}ms audio)")
else:
print(f"Job failed: {payload.get('error')}")
return "OK", 200Webhook Requirements
- HTTPS only — webhook_url must use HTTPS protocol
- Max URL length — 2,048 characters
- No private IPs — localhost, 127.0.0.1, 10.x, 172.16-31.x, 192.168.x are blocked
- No internal domains — murmr.dev subdomains are blocked
- Payload size — audio is base64-encoded (~1.33x raw file size). Set your server's body size limit accordingly.
Job ID Format
Job IDs follow the pattern job_ followed by 16 hexadecimal characters:
/^job_[a-f0-9]{16}$/
Example: job_a1b2c3d4e5f6g7h8Job Status Values
| Status | Description |
|---|---|
| queued | Job is waiting for an available GPU worker |
| processing | GPU worker is generating audio |
| completed | Audio generated — poll to download, or check webhook |
| failed | Job failed — check the error field for details |
Job metadata expires after 24 hours. Audio data on the backend may be purged sooner. Retrieve completed audio promptly.
Error Responses
Bad Request
Invalid webhook_url (not HTTPS, private IP, too long), invalid response_format, missing text
Not Found
Job ID not found, expired (24h TTL), or belongs to a different account
Gone
Job completed but audio data has been purged from the backend. Re-submit the job.
Server Busy
GPU queue is full. Check the Retry-After header.
Service Unavailable
Batch processing is temporarily unavailable
See Also
- Saved Voices API — Batch and streaming TTS endpoints
- Audio Formats — Supported response_format values
- Error Reference — Complete error handling guide