Suno API task status, polling and recovery
Treat song generation as a job with its own lifecycle. Your application should remain useful while the audio is being composed.
Save a request ID before submission#
Send a UUID in the Idempotency-Key header. It becomes your task_id, so store it before sending the request. Reusing it with the same payload does not submit another song or reserve credits twice; a different payload returns 409. If the response is lost, check this task ID. A pending response marked unknown requires confirmation; do not replace it with a new request.
Task states and HTTP responses#
Pending task responses include task_id, status, code: 102 and type: not_ready. A completed response contains data rather than a status field. HTTP 200 is therefore not sufficient evidence of successful generation.
| Task / result | HTTP | What to do |
|---|---|---|
| reserved / unknown | 202 | Submission needs confirmation. Keep the same UUID; do not start another job. |
| submitted | 202 | Provider job is associated with the task. Poll with a delay and deadline. |
| Completed audio: data[] | 200 | Read id, title and audio_url for each returned track. |
| Generation failure: type=failed | 200 | Stop polling; inspect message. Confirmed provider failures return reserved credits. |
| Missing / another account’s task | 404 | Check the saved UUID and key ownership. Do not treat it as pending. |
Poll with a deadline#
Use GET /api/v1/sonic/task/{task_id}. HTTP 202 means pending. Wait between polls, set an overall deadline, and stop on a terminal response. Never interpret an authentication or validation error as a pending task.
const deadline = Date.now() + 300_000;
while (Date.now() < deadline) {
const response = await fetch(taskUrl, { headers });
if (response.status === 202) {
await new Promise(resolve => setTimeout(resolve, 5000));
continue;
}
if (!response.ok) throw new Error(`Task lookup: ${response.status}`);
const result = await response.json();
// Inspect the task result for success or failure before delivering audio.
return result;
}
throw new Error("Polling deadline reached; retain the task ID for recovery.");Separate request errors from generation failures#
A 401 indicates an authentication problem. Rate-limit responses need backoff. A completed HTTP request can still contain a failed generation. Inspect the task body and require an actual audio_url before presenting a playable song.
Choose a recovery action from the evidence#
A browser timeout does not prove that submission failed. Start with the saved request record. Task lookups do not create a new song or reserve another 20 credits.
| What you observed | Recovery action | Credit implication |
|---|---|---|
| POST response was lost or a client deadline expired | GET the original UUID using the same deployment and account. Keep the saved payload. | A reservation may already exist; a new UUID can charge again. |
| 202 with reserved or unknown | Keep the task ID. Check again later; do not automatically submit a replacement. | The existing reservation is retained until the outcome is confirmed. |
| 202 with submitted | Continue delayed GET polling within your own deadline. | Polling does not reserve additional credits. |
| 200 with type: failed | Read the message. Confirm returned credits in the balance or history before an intentional new request. | A confirmed provider failure returns the reservation once. |
| 404 on task lookup | Verify the base URL, key owner and saved UUID. A task from another account is also hidden. | 404 alone does not prove no charge occurred in another environment or account. |
| 409 on submission | Recover the original UUID and compare the saved payload; do not change its inputs and retry. | The conflicting request does not replace the original job. |
Recover a saved task without submitting again#
This Node.js 20+ example performs one GET only. Set SUNO_API_BASE and SUNO_API_KEY as in Get started, and SUNO_TASK_ID to the UUID saved before submission. A lookup timeout means the outcome is still unconfirmed; keep the UUID for a later lookup. Never log the key.
const { SUNO_API_BASE: base, SUNO_API_KEY: key, SUNO_TASK_ID: id } = process.env;
if (!base || !key || !id) throw new Error('Set base URL, key and saved task ID.');
const response = await fetch(`${base}/sonic/task/${encodeURIComponent(id)}`, {
headers: { Authorization: `Bearer ${key}` },
signal: AbortSignal.timeout(45000),
});
const body = await response.json();
if (response.status === 202) {
console.log('Keep this task ID and check later:', id, body.status);
} else if (!response.ok) {
throw new Error(`Lookup ${response.status}: ${body.error || 'Check the saved request record.'}`);
} else if (body.type === 'failed') {
console.log(id, body.message);
} else if (Array.isArray(body.data) && body.data.length &&
body.data.every(track => typeof track.audio_url === 'string' && track.audio_url.startsWith('https://'))) {
for (const track of body.data) console.log(track.title, track.audio_url);
} else {
throw new Error(`Unexpected result; retain task ${id} for investigation.`);
}Resume a task in the workbench#
Paste a task ID and enter the matching SunoAPI key, then choose Check task. The workbench polls every five seconds for up to five minutes and stops on completion or an error. Stop tracking ends browser checks; it does not cancel generation. If a create request loses its response, check the SunoAPI task history before generating again. Looking up an existing task does not submit another song.
Deliver audio deliberately#
Associate the returned tracks with the original request. Save the metadata your product needs and make playback state explicit. If you need durable storage, check the service retention terms and applicable rights before copying output.
Failure response#
A confirmed provider rejection or failed task returns the original reservation once. This example is illustrative. An uncertain submission is not the same as a confirmed failure.
{
"task_id": "a4ef5803-48f7-42cc-9697-cc1b1be9d042",
"type": "failed",
"message": "Song generation failed. Reserved credits have been returned."
}HTTP error reference#
Most request errors use {"error":"message"}. Store the status and task ID for troubleshooting, never your API key. A timeout or 503 after submission may be uncertain: look up the original UUID before taking another paid action.
| HTTP | Meaning | Next step |
|---|---|---|
| 400 | Invalid JSON, field value, model, UUID or content type | Correct the request; do not blindly retry. |
| 401 | Missing, invalid or revoked key | Use a key from this deployment’s account. |
| 402 | Fewer than 20 available credits | Check credits + extra_credits. |
| 403 | Cross-origin browser request | Send requests from your server. |
| 404 | Task not found in the key owner’s account | Verify environment, UUID and key. |
| 409 | UUID already belongs to a different payload or account | Recover the original task; use a new UUID only for an intentional new song. |
| 413 | Body exceeds 32,768 bytes | Reduce the JSON body size. |
| 503 | Account service, credit reservation or provider configuration unavailable | Read the message and recover the original task before retrying. |
A complete Node.js submission and polling example#
Requires Node.js 20 or later. Set SUNO_API_BASE to the base URL from Get started, and SUNO_API_KEY to your server-side key. Running this script submits one paid request. Save the printed UUID durably before submission in a production integration. No automatic create retry is performed.
import { randomUUID } from 'node:crypto';
const base = process.env.SUNO_API_BASE;
const key = process.env.SUNO_API_KEY;
if (!base || !key) throw new Error('Set SUNO_API_BASE and SUNO_API_KEY.');
const id = randomUUID();
console.log('Save this task ID before submission:', id);
const headers = { Authorization: `Bearer ${key}` };
async function read(response) {
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${body.error || 'Request failed'}`);
return body;
}
const create = await fetch(`${base}/sonic/create`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json', 'Idempotency-Key': id },
body: JSON.stringify({ custom_mode: false, mv: 'sonic-v6-mini',
gpt_description_prompt: 'Quiet woodland, felt piano, soft strings', make_instrumental: true }),
signal: AbortSignal.timeout(75000),
});
let body = await read(create);
let status = create.status;
const deadline = Date.now() + 300000;
while (status === 202 && Date.now() < deadline) {
await new Promise(resolve => setTimeout(resolve, 5000));
const remaining = deadline - Date.now();
if (remaining <= 0) break;
const response = await fetch(`${base}/sonic/task/${id}`, {
headers, signal: AbortSignal.timeout(Math.min(45000, remaining)),
});
body = await read(response);
status = response.status;
}
if (status === 202) throw new Error(`Still pending. Recover task ${id}; do not generate again.`);
if (body.type === 'failed') throw new Error(body.message);
if (!Array.isArray(body.data) || !body.data.length ||
body.data.some(track => typeof track.audio_url !== 'string' || !track.audio_url.startsWith('https://'))) {
throw new Error(`Unexpected result. Keep task ${id} for recovery.`);
}
for (const track of body.data) console.log(track.title, track.audio_url);