Music
Music follows the same async pattern as video: create, poll, download.
Create a track
Section titled “Create a track”import requests
job = requests.post( "https://api.mixen.ai/v1/audio/music", headers={"Authorization": f"Bearer {API_KEY}"}, json={"model": "suno", "prompt": "Melancholic synth-pop about a city at night, female vocals"},).json()print(job["id"], job["status"])const res = await fetch('https://api.mixen.ai/v1/audio/music', { method: 'POST', headers: { Authorization: `Bearer ${process.env.MIXEN_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'suno', prompt: 'Melancholic synth-pop about a city at night, female vocals' }),})const job = await res.json()console.log(job.id, job.status)curl https://api.mixen.ai/v1/audio/music \ -H "Authorization: Bearer $MIXEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "suno", "prompt": "Melancholic synth-pop about a city at night, female vocals"}'| Field | Description |
|---|---|
model |
suno / suno-v6 — a song with vocals, two tracks per request. lyria / lyria-pro — instrumental, one track |
mode |
simple (from prompt) or custom (from lyrics + style + title). Lyria ignores this |
prompt |
Track description for simple mode |
lyrics |
Song lyrics for custom mode |
style |
Genre, style, tags |
title |
Track title |
Custom mode
Section titled “Custom mode”When you need specific lyrics rather than generated ones:
{ "model": "suno-v6", "mode": "custom", "title": "Night Tram", "style": "indie rock, 90s, male vocals, live drums", "lyrics": "First verse...\n\nChorus..."}Poll and download
Section titled “Poll and download”curl https://api.mixen.ai/v1/audio/music/music_... \ -H "Authorization: Bearer $MIXEN_API_KEY"
curl -L https://api.mixen.ai/v1/audio/music/music_.../content \ -H "Authorization: Bearer $MIXEN_API_KEY" -o track.mp3Statuses are the same: queued → in_progress → completed or failed.
Suno returns two variants of the track per request — that is how the model behaves, not an error. Both appear in the poll response; a specific track is downloaded with the ?index=N parameter of /audio/music/{id}/content (numbering starts at zero, default 0).
A job that has not finished within 15 minutes is marked failed on the next poll and is not charged.
Skip polling: webhook, wait, idempotency
Section titled “Skip polling: webhook, wait, idempotency”Three optional body fields of POST /v1/audio/music, shared by video and music:
| Field | What it does |
|---|---|
webhook_url |
A public https URL. On completed or failed we POST the same job object that GET returns. Private hosts and http:// are rejected with 400 invalid_webhook_url before the job is created |
wait |
true holds the request until the job is terminal and returns the final object (up to wait_timeout, default 60 s, max 300). On timeout the job is returned as is (in_progress), no error |
idempotency_key |
Up to 128 chars. A repeated request with the same key returns the already created job instead of a new one (and does not charge again); the response carries X-Mixen-Idempotent-Replay: true. Unique per account |
Webhook request headers:
| Header | Value |
|---|---|
X-Mixen-Event |
music.completed or music.failed |
X-Mixen-Delivery |
the job id |
X-Mixen-Signature |
t=<unix>,v1=<hex> — HMAC-SHA256 over "<t>.<raw body>"; the secret is the sha256 hex of your API key (we never store the key itself, you compute the hash locally) |
Your endpoint must answer 2xx; otherwise delivery is retried after 5, 20, 60 and 180 seconds. At-least-once: handle repeats idempotently by X-Mixen-Delivery. Verifying the signature:
import hashlib, hmac
def verify(raw_body: bytes, header: str, api_key: str) -> bool: secret = hashlib.sha256(api_key.encode()).hexdigest() parts = dict(kv.split('=', 1) for kv in header.split(',')) expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get('v1', ''))Music is billed per request rather than per second: the per_request_rub field in the catalog. Suno’s two tracks are one charge.
Price before you run
Section titled “Price before you run”POST /v1/audio/music/price accepts the same body as POST /v1/audio/music and returns the amount without generating or charging anything. It uses the same formula as the actual charge, so the quote matches the bill; validation is shared too — invalid parameters return the same 400/404 as a real run.
{ "object": "price", "model": "suno", "model_id": "suno-music-generate", "price": {"rub": 8.0047, "usd": 0.09}, "per_unit": {"rub": 8.0047, "unit": "generation"}, "quantity": 1, "params": {"mode": "simple", "tracks": 2}}The rouble amount uses the live rate — quote right before you run, don’t cache it.