Tunova

Suno API error handling

Updated 2026-07-08

The short version: a robust Suno integration doesn’t just check HTTP status codes — it sorts every failure into one of four buckets, retries the two that are transient, and never retries the two that aren’t. The one thing that changes how aggressive you can be is billing: when retries and failed renders are free, safe retries are cheap. This page gives you the taxonomy, a decision table, and a copy-paste wrapper.

The real failure modes (not just HTTP codes)

Generating a song is a long, async operation, so failures happen in more than one place. There are four that matter, and they call for different handling:

  • Input rejected — your request is malformed (bad field, missing prompt, unknown model). Retrying the same body will fail the same way. Fix the request.
  • Transient capacity / rate-limit — you sent too fast (429) or the service couldn’t accept the job right now (503). Nothing is wrong with your request; back off and retry.
  • Generation failed — the job was accepted, then the render failed downstream. On Tunova the job reaches status: failed, the tokens are refunded, and a retry is safe.
  • Delivery failed — the song rendered but your webhook didn’t get the news (your endpoint was down, or the signature check failed on your side). The result still exists — poll for it.

A structured error taxonomy

Tunova returns a consistent error envelope — { code, detail, request_id } — plus anX-Request-Id header on every response. Here is the full client-facing set and exactly what to do with each:

Code (HTTP)MeaningRetry?Billed?
VALIDATION_ERROR (400)Malformed request — bad or missing fieldNo — fix the bodyNo
UNAUTHORIZED (401)Invalid or missing API keyNo — fix the keyNo
INSUFFICIENT_TOKENS (402)Balance too low (body carries balance + cost)No — top up firstNo
RATE_LIMITED (429)Per-key rate limit (60 req/min, sliding window)Yes — back off per Retry-AfterNever
ENGINE_UNAVAILABLE (503)Transient — couldn’t accept the job right nowYes — back off per Retry-AfterRefunded
Job status: failedAccepted, then the render failedYes — safe & freeRefunded
404 on a jobUnknown or expired job id (jobs retained ~7 days)No

429 / rate limits: back off, and it never costs a credit

Tunova’s per-key limit is 60 requests/minute over a sliding 60-second window. You don’t have to hit it to find out where it is: RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset are returned on every response — including successes — so you can self-pace. If you do get a 429, honor Retry-After and retry. A rate-limited request never reserves a token, so retrying it costs nothing but time.

Timeouts on long generations: idempotency keys make retries safe

A render takes minutes, so submits are async: you get 202 { job_id } back immediately, then poll GET /api/jobs/{id} or receive a webhook. The risk on a retried submit is a duplicate render (and a double charge) if your first request actually went through and you just didn’t see the response. Send an Idempotency-Key header and that risk disappears — a retry with the same key returns the same job:

# Same Idempotency-Key on every retry of ONE logical request → same job, no double charge. curl -X POST https://api.tunova.ai/api/generate \ -H "X-API-Key: sk_live_..." \ -H "Idempotency-Key: gen-2f9c1e7a" \ -H "content-type: application/json" \ -d '{"prompt": "calm rainy-night lofi"}' # -> 202 {"job_id": "8f3a...", "status": "queued"} (a retry returns the same job_id)

Retry decision table

The whole strategy in one rule: retry the transient bucket, fix the terminal bucket.

  • Retry (with backoff): 429, 503, and any job that endsfailed. All are free on Tunova, so retry up to a sane cap.
  • Don’t retry — fix it: 400 (fix the body), 401 (fix the key), 402 (top up). Retrying these just wastes calls.

Debug one failed generation with request-ids

Every response and error carries X-Request-Id (and request_id in the JSON body). Log it next to your own job id. When something goes wrong, you have a single handle that ties your call to what happened on the server — support becomes “here’s request req_…,” not a reproduction hunt.

Why billed-on-success changes your retry math

On a pay-per-attempt API, every retry is a gamble with real money, so you retry timidly — and users see more failures. Tunova bills only on a successful, delivered song: a failed render is auto-refunded, and 429/503 never reserve tokens. So the correct retry policy is simply “retry the transient bucket until it works,” because a retry costs nothing.

Worked example. Say 1 in 20 attempts fails transiently. On a bill-per-attempt provider at $0.05/attempt, 1,000 delivered songs cost you ~1,053 attempts = ~$52.65 — you pay for the 53 failures. On billed-on-success, the 53 failures are refunded, so 1,000 delivered songs cost exactly 1,000 — and your retry loop can be as patient as it needs to be. (See the effective-cost framework for the general formula.)

Copy-paste retry wrapper

A minimal, dependency-light submit that classifies the error, honors Retry-After, and reuses one idempotency key so a retry can never double-charge. Python first, then Node.

import time, uuid, requests API = "https://api.tunova.ai" KEY = "sk_live_..." TERMINAL = {"VALIDATION_ERROR", "UNAUTHORIZED", "INSUFFICIENT_TOKENS"} # don't retry — fix it def submit(prompt, tries=5): idem = "gen-" + uuid.uuid4().hex # one key for the whole retry loop for attempt in range(tries): r = requests.post(f"{API}/api/generate", headers={"X-API-Key": KEY, "Idempotency-Key": idem, "content-type": "application/json"}, json={"prompt": prompt}) if r.status_code == 202: return r.json()["job_id"] body = r.json() rid = r.headers.get("X-Request-Id") if body.get("code") in TERMINAL: raise RuntimeError(f"{body.get('code')}: {body.get('detail')} (request {rid})") # 429 / 503 — transient and free. Honor Retry-After, back off, retry. wait = int(r.headers.get("Retry-After", 2 ** attempt)) time.sleep(wait) raise RuntimeError("still unavailable after retries")
const API = "https://api.tunova.ai", KEY = "sk_live_..."; const TERMINAL = new Set(["VALIDATION_ERROR", "UNAUTHORIZED", "INSUFFICIENT_TOKENS"]); async function submit(prompt, tries = 5) { const idem = "gen-" + crypto.randomUUID(); // one key for the whole retry loop for (let attempt = 0; attempt < tries; attempt++) { const r = await fetch(`${API}/api/generate`, { method: "POST", headers: { "X-API-Key": KEY, "Idempotency-Key": idem, "content-type": "application/json" }, body: JSON.stringify({ prompt }), }); if (r.status === 202) return (await r.json()).job_id; const body = await r.json(); const rid = r.headers.get("X-Request-Id"); if (TERMINAL.has(body.code)) throw new Error(`${body.code}: ${body.detail} (request ${rid})`); // 429 / 503 — transient and free. Honor Retry-After, back off, retry. const wait = Number(r.headers.get("Retry-After") ?? 2 ** attempt); await new Promise((res) => setTimeout(res, wait * 1000)); } throw new Error("still unavailable after retries"); }

FAQ

Does a Suno API charge me for failed generations?

It depends on the provider — many do. With Tunova, no: billing is on success only, so a render that fails after submit is auto-refunded, and rate-limit (429) and transient-capacity (503) responses never reserve a token in the first place. That means a retry is free, which changes how aggressively you can retry.

How do I handle 429 rate-limit errors from a Suno API?

Back off and retry, honoring the Retry-After header. Tunova returns RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset on every response (successes too) so you can self-pace before you ever hit a 429 — and a 429 never costs a token.

Are retries safe, or will I get double-charged?

Send an Idempotency-Key header on the submit. A retried request with the same key returns the SAME job instead of starting a second render — no duplicate song, no double charge. Reuse the key for the whole retry loop of one logical request.

How do I debug a single failed generation?

Every response and every error carries an X-Request-Id (also echoed in the error body as request_id). Log it, and quote it in a support request — the answer is 'here's my request id,' not 'it didn't work.'

Which Suno API errors should I not retry?

Input/validation (400), authentication (401), and insufficient balance (402) are terminal — retrying won't help, you fix the request, key, or balance. Rate-limit (429), transient capacity (503), and a job that reaches status failed are all retry-safe.

Related

Tunova is an independent service, not affiliated with or endorsed by Suno. “Suno” is a trademark of its owner.