SUNOAPI / TASK LIFECYCLE

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 / resultHTTPWhat to do
reserved / unknown202Submission needs confirmation. Keep the same UUID; do not start another job.
submitted202Provider job is associated with the task. Poll with a delay and deadline.
Completed audio: data[]200Read id, title and audio_url for each returned track.
Generation failure: type=failed200Stop polling; inspect message. Confirmed provider failures return reserved credits.
Missing / another account’s task404Check 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.

Example
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 observedRecovery actionCredit implication
POST response was lost or a client deadline expiredGET 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 unknownKeep the task ID. Check again later; do not automatically submit a replacement.The existing reservation is retained until the outcome is confirmed.
202 with submittedContinue delayed GET polling within your own deadline.Polling does not reserve additional credits.
200 with type: failedRead 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 lookupVerify 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 submissionRecover 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.

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

Example
{
  "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.

HTTPMeaningNext step
400Invalid JSON, field value, model, UUID or content typeCorrect the request; do not blindly retry.
401Missing, invalid or revoked keyUse a key from this deployment’s account.
402Fewer than 20 available creditsCheck credits + extra_credits.
403Cross-origin browser requestSend requests from your server.
404Task not found in the key owner’s accountVerify environment, UUID and key.
409UUID already belongs to a different payload or accountRecover the original task; use a new UUID only for an intentional new song.
413Body exceeds 32,768 bytesReduce the JSON body size.
503Account service, credit reservation or provider configuration unavailableRead 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.

Example
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);