Suno Generations and Playback
Follow a song Generation
Section titled “Follow a song Generation”Use the Generation API for product state, candidate discovery, early playback, and final output selection. Use the Task API separately for execution, cancellation, billing, and terminal diagnostics.
Read one Generation
Section titled “Read one Generation”GET /api/v1/suno/generations/{generationId} is the canonical song product
view. It is available before completion and exposes every candidate
independently:
curl https://api.omnapi.com/api/v1/suno/generations/task_01J... \ -H "x-api-key: $OMNAPI_KEY"Expand the complete JSON example
{ "generationId": "task_01J...", "taskId": "task_01J...", "status": "PROCESSING", "phase": "generating", "progress": 42, "deliveryStatus": "streaming", "clips": [ { "clipId": "clip_a", "index": 0, "role": "candidate", "deliveryStatus": "streaming", "audioUrl": null, "playable": true, "playback": { "state": "live", "url": "/api/v1/suno/playback/EXAMPLE_ONLY", "mimeType": "audio/mpeg", "delivery": "chunked", "seekable": false, "expiresAt": "2026-09-03T12:10:00.000Z", "retryAfterMs": null } }, { "clipId": "clip_b", "index": 1, "role": "candidate", "deliveryStatus": "queued", "audioUrl": null, "playable": false, "playback": { "state": "preparing", "url": null, "mimeType": null, "delivery": "none", "seekable": false, "expiresAt": null, "retryAfterMs": 3000, "sessionCreateUrl": null } } ], "finalClipId": null, "autoFinalize": false}| Field | Meaning |
|---|---|
version | Monotonic product snapshot version |
status | Shared Task status: PENDING, PROCESSING, COMPLETED, FAILED, or CANCELLED |
phase | Product phase: accepted, generating, finalizing, completed, or failed |
progress | Best-effort 0..100 progress indicator |
deliveryStatus | Aggregate clip delivery state; nullable before acknowledgement |
clips[] | Every independently queryable candidate and automatic final resource |
finalClipId / finalUrl | Selected final output when one exists |
billing | Required, charged, refunded, settlement, and frozen pricing data |
retryable / refundable | Client retry guidance and failure-refund eligibility |
warningCodes | Non-fatal result warnings |
Clip delivery states are monotonic:
submitted → queued → streaming → complete, with error as a terminal
per-clip failure. One candidate can be playable while another remains queued.
The Generation is not complete until the Task’s required finalization is
complete. For Extend, the automatic Concat appears as role: "final".
Play while a song is generating
Section titled “Play while a song is generating”Query the Generation and use clips[].playback.url directly. No SSE
subscription or separate playback-session request is required. The examples
above are shortened snapshots; the example media URL is a placeholder, not a
working link. Always use the complete URL from your own response.
Select a clip with playable: true and a non-null playback.url. During
streaming, audioUrl can be null even when the clip is playable. Individual
clip reads are useful for metadata and completed audio; use the Generation
read for live playback URLs.
| Playback field | Client contract |
|---|---|
state | preparing, live, final, error, or unavailable |
url | Current media URL, or null when no URL is available. Resolve relative values against your API base URL. Do not construct or edit the returned URL. |
mimeType | audio/mpeg for live playback; completed audio can be audio/mp4 or audio/mpeg. Use the returned value. |
delivery | chunked for live media, progressive for completed media, or none |
seekable | Enables seeking only when true; live playback does not support seeking or resumable range downloads |
expiresAt | Expiry time for a temporary URL; null when no expiry is advertised |
retryAfterMs | Suggested wait before querying again when media is not ready; otherwise null |
sessionCreateUrl | Deprecated compatibility field. New integrations should ignore it and use url. |
Keep your API key on your server for Generation queries. Pass only the selected
clip or authorized Generation snapshot to your frontend. Opening the returned
media URL does not require an x-api-key header, cookie, or additional
credential. Never put your API key in a media URL. Temporary playback URLs
grant access to the audio, so exclude them from logs, analytics, and public
shares.
Switching candidates and playback limits
Section titled “Switching candidates and playback limits”When switching candidates, cancel the previous media request and open the selected clip’s current URL. Pausing audio alone may leave its download active. Reopening the same URL uses one active slot, just like a first open. There is no cumulative playback-open quota per Generation.
Live URLs on stream.omnapi.com allow up to 4 simultaneous requests per playback
ticket, 4 per clip, and 16 per public source IP, subject to a shared service
capacity of 1,000 active requests. These checks apply together. A new ticket for
the same clip still shares the clip limit. Mobile users behind the same carrier
or other shared network can share the IP limit; these are not API-key tier limits.
Close the previous request before retrying. On 429 playback-capacity-exceeded,
wait at least the Retry-After interval, then use a bounded retry with increasing
delay if the limit persists. The current hint is 3 seconds; it is a minimum
retry delay, not a guarantee that another connection has finished. See
live playback limits for response
headers and support diagnostics. Do not open extra GET requests just to inspect
media headers. HEAD does not consume a playback slot, but does not prove that
audio can currently be delivered.
Browser example
Section titled “Browser example”Use browser-native audio controls. Opening a current media URL directly in a browser also works when the browser supports its audio format; the listener may need to press Play because autoplay is subject to browser policy.
<audio id="song" controls preload="none"></audio><button id="play" disabled>Play selected song</button><p id="playback-status" role="status"></p>const apiBaseUrl = "https://api.omnapi.com";const audio = document.querySelector("#song");const playButton = document.querySelector("#play");const status = document.querySelector("#playback-status");let selectedClip = null;
// Call after your server queries the Generation. Keep the user's clip choice.function updatePlayback(generation, selectedClipId) { selectedClip = generation.clips.find(c => c.clipId === selectedClipId) ?? null; playButton.disabled = !(selectedClip?.playable && selectedClip.playback?.url); // Do not reset audio.src here: refreshed live URLs must not interrupt audio.}
playButton.addEventListener("click", async () => { const playback = selectedClip?.playback; if (!selectedClip?.playable || !playback?.url) return; if (playback.expiresAt && Date.parse(playback.expiresAt) <= Date.now()) { status.textContent = "Refresh the Generation, then press Play again."; return; } audio.src = new URL(playback.url, apiBaseUrl).href; try { await audio.play(); status.textContent = ""; } catch { status.textContent = "Use the audio controls or refresh the Generation."; }});
audio.addEventListener("error", () => { status.textContent = "Refresh the Generation before retrying playback.";});Poll every few seconds while waiting, honoring retryAfterMs and any
Retry-After response header. Do not discard a fresh playback URL solely
because the Generation version is unchanged. If playback stalls or errors,
re-query the same Generation and retry with its latest URL; do not create a
new song. Native audio controls do not expose JSON error bodies, so refresh
on a media error even when its exact HTTP code is unavailable.
When state becomes final, retain that URL for the next playback or switch
at a controlled point. Replacing audio.src restarts loading; avoid doing so
on every poll. An unexpired live URL may redirect to completed audio, which
normal browser media requests follow. Completion does not make an expired
URL reusable. finalClipId and top-level finalUrl may remain null for a
multi-candidate song; each clip’s playback.url is still usable.
Playback errors
Section titled “Playback errors”| HTTP / code | Action |
|---|---|
404 SUNO_PLAYBACK_SESSION_INVALID | URL expired or is no longer usable. Query the Generation again; do not alter the URL. |
409 SUNO_PLAYBACK_NOT_READY | Wait for details.retryAfterMs when present, then query again. |
429 RATE_LIMITED / playback-capacity-exceeded | Respect Retry-After; close previous media requests and use bounded backoff. |
503 SUNO_PLAYBACK_UNAVAILABLE / playback-capacity-unavailable | Temporary playback failure. Back off, then query again. |
For state: "preparing" or a null URL, keep waiting. For error, inspect that
clip’s errorCode and errorMessage; another candidate may still succeed.
For unavailable, do not start playback from a missing URL; refresh later or
offer another playable candidate. No playback attempt guarantees that the
entire generation will eventually complete.
List Generation history
Section titled “List Generation history”GET /api/v1/suno/generations?limit=20&cursor=... lists the current user’s
Generation history. Its cursor is opaque.
curl "https://api.omnapi.com/api/v1/suno/generations?limit=20" \ -H "x-api-key: $OMNAPI_KEY"{ "items": [ { "generationId": "task_01J...", "taskId": "task_01J...", "status": "PROCESSING", "phase": "generating", "clips": [] } ], "nextCursor": "opaque-cursor", "hasMore": true}limit accepts 1..100 and defaults to 20. Omit cursor on the first
request, then pass nextCursor unchanged while hasMore is true. Each item
has the complete Generation shape; the shortened object above highlights only
pagination-relevant fields.