Skip to content
RU

Music

Music follows the same async pattern as video: create, poll, download.

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"])
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

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..."
}
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.mp3

Statuses 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.

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.

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.