Skip to content

Suno Generations and Playback

Product overview · Task model

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.

GET /api/v1/suno/generations/{generationId} is the canonical song product view. It is available before completion and exposes every candidate independently:

Terminal window
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
}
FieldMeaning
versionMonotonic product snapshot version
statusShared Task status: PENDING, PROCESSING, COMPLETED, FAILED, or CANCELLED
phaseProduct phase: accepted, generating, finalizing, completed, or failed
progressBest-effort 0..100 progress indicator
deliveryStatusAggregate clip delivery state; nullable before acknowledgement
clips[]Every independently queryable candidate and automatic final resource
finalClipId / finalUrlSelected final output when one exists
billingRequired, charged, refunded, settlement, and frozen pricing data
retryable / refundableClient retry guidance and failure-refund eligibility
warningCodesNon-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".

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 fieldClient contract
statepreparing, live, final, error, or unavailable
urlCurrent 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.
mimeTypeaudio/mpeg for live playback; completed audio can be audio/mp4 or audio/mpeg. Use the returned value.
deliverychunked for live media, progressive for completed media, or none
seekableEnables seeking only when true; live playback does not support seeking or resumable range downloads
expiresAtExpiry time for a temporary URL; null when no expiry is advertised
retryAfterMsSuggested wait before querying again when media is not ready; otherwise null
sessionCreateUrlDeprecated 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.

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.

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.

HTTP / codeAction
404 SUNO_PLAYBACK_SESSION_INVALIDURL expired or is no longer usable. Query the Generation again; do not alter the URL.
409 SUNO_PLAYBACK_NOT_READYWait for details.retryAfterMs when present, then query again.
429 RATE_LIMITED / playback-capacity-exceededRespect Retry-After; close previous media requests and use bounded backoff.
503 SUNO_PLAYBACK_UNAVAILABLE / playback-capacity-unavailableTemporary 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.

GET /api/v1/suno/generations?limit=20&cursor=... lists the current user’s Generation history. Its cursor is opaque.

Terminal window
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.