# Create Task Source: https://docs.sunor.cc/api-reference/create-task POST https://sunor.cc/api/v1/task Submit a new AI generation task # Create Task Submit a new task to an AI model. The task is processed asynchronously -- use the [Get Task](/api-reference/get-task) endpoint to poll for results. Sunor validates the JSON body, model, task type, and model-specific input before freezing credits, inserting a task, or calling an upstream provider. Invalid requests therefore fail without creating a task or changing the credit balance. ## Request ``` POST /api/v1/task ``` ### Headers Your API key. Must be `application/json`. ### Body parameters The AI model to use. One of `"suno"` or `"udio"`. See the [Suno](/models/suno) and [Udio](/models/udio) model pages for capabilities and pricing. The type of task to create. Suno supports: `"music"`, `"lyrics"`, `"upload"`, `"concat"`. Udio supports `"music"` only. Task-specific input parameters. See the sections below for each task type. Pin the delivery format of `audio_url`. Currently the only accepted value is `"mp3"`. Omit it and you get the provider's own file, in whatever format that provider produces. **That is not a fixed format** — it changes when a provider changes what it makes, and it changes when we route your request to a different provider to keep the service up. See [Audio format](/api-reference/get-task#audio-format). Send `"mp3"` and `audio_url` points at MP3. A source that is already MP3 is delivered as-is rather than re-encoded, so you never pay a second lossy pass for a file that was already right. **If the conversion fails the task still succeeds** — you get our copy of the source file, at our URL and our retention, in the source format. The response echoes back the `audio_format` you sent either way, because that field records the request rather than the outcome; read `Content-Type` to know what a file actually is. Applies to task types that return audio. A `lyrics` task has no `audio_url`, so the field changes nothing about what you receive — it is still accepted and still echoed back. Any other value is rejected with `invalid_audio_format` before any credits are charged. The field as a whole can also be refused with `audio_format_unavailable`, which means conversion is switched off on our side rather than anything being wrong with your request; omit the field to receive the provider's original file. Sunor validates the documented model/task fields before charging credits or submitting to a provider. Extra root or `input` properties are accepted for backward/forward compatibility, but they are not interpreted as provider controls unless a request variant explicitly prohibits them. Use documented fields for provider behavior. *** ## Task types and input schemas ### `music` — Generate music The `music` task supports three modes depending on which input fields you provide. Input fields differ by model. The examples below show **Suno** input shape (using `gpt_description_prompt`, `make_instrumental`, etc.). **Udio** uses a different field set — see the [Udio model page](/models/udio) for the Udio-specific shape. Mixing them produces a `400` validation error. Generate music from a natural language description. The AI interprets your prompt and creates lyrics, melody, and arrangement. A natural language description of the music you want (e.g., "A chill lo-fi beat for studying"). Set to `true` to generate instrumental music without vocals. ```json theme={null} { "model": "suno", "task_type": "music", "input": { "gpt_description_prompt": "A cheerful acoustic guitar song about summer", "make_instrumental": false } } ``` Provide your own lyrics and/or style tags for precise control over the output. For a **style-driven instrumental**, omit `prompt` and set `make_instrumental: true`. The lyrics for the song. Use standard song structure notation like `[Verse]`, `[Chorus]`, etc. Omit (or leave empty) for an instrumental. Comma-separated style/genre tags (e.g., `"pop, upbeat, female vocals"`). Comma-separated tags for styles to avoid (e.g., `"heavy metal, screaming"`). Set to `true` to generate an instrumental with no vocals. Combine with `tags` (and no `prompt`) for a style-driven instrumental. Title of the song. Provide at least one of `prompt` or `tags`. A music request with no `prompt`, `tags`, or `gpt_description_prompt` is rejected with a `400`. ```json theme={null} { "model": "suno", "task_type": "music", "input": { "prompt": "[Verse]\nWalking down the sunlit road\nFeeling light without a load\n\n[Chorus]\nOh summer days, carry me away", "tags": "pop, acoustic, upbeat", "title": "Summer Days" } } ``` Style-driven instrumental (no lyrics): ```json theme={null} { "model": "suno", "task_type": "music", "input": { "tags": "lofi, jazz piano, rain, mellow", "make_instrumental": true, "title": "Late Night Study" } } ``` Extend an existing music clip from a specific timestamp. The clip ID to continue from — from a previous music task output, or from an uploaded audio clip (the `upload` task) to extend your own audio into a full song. Timestamp in seconds to continue from. Additional lyrics or instructions for the continuation. ```json theme={null} { "model": "suno", "task_type": "music", "input": { "continue_clip_id": "abc123-clip-id", "continue_at": 30, "prompt": "[Chorus]\nKeep the music playing on" } } ``` Udio uses a single `prompt` field for the style description. See the [Udio model page](/models/udio) for full options including `lyrics`, `tags`, `lyrics_type`, and `seed`. Short style/mood description (e.g., "lofi hip hop, chill, rainy night, jazz piano"). Udio's GPT layer expands it into full prompt + lyrics. ```json theme={null} { "model": "udio", "task_type": "music", "input": { "prompt": "lofi hip hop, chill, rainy night, jazz piano" } } ``` ### `lyrics` — Generate lyrics A description of the lyrics you want (e.g., "A love song about the ocean at sunset"). ```json theme={null} { "model": "suno", "task_type": "lyrics", "input": { "prompt": "A love song about the ocean at sunset" } } ``` ### `upload` — Upload audio A publicly accessible URL of the audio file to upload. ```json theme={null} { "model": "suno", "task_type": "upload", "input": { "url": "https://example.com/my-audio.mp3" } } ``` The upload returns a clip `id` (at `output.result[0].id`). Pass it as `continue_clip_id` in a `music` task to **extend your uploaded audio into a full song** (see the Continuation Mode tab under the `music` task). Generating a new song in a *different* style from an uploaded reference (an "audio cover") is not currently supported. ### `concat` — Concatenate clips The clip ID to concatenate (obtained from a previous music task output). ```json theme={null} { "model": "suno", "task_type": "concat", "input": { "clip_id": "abc123-clip-id" } } ``` *** ## Response ### Response headers | Header | Description | | ----------------------- | ----------------------------------------------------------------------------------- | | `X-Request-Id` | Correlation identifier for this request. Log it with failures and support requests. | | `X-RateLimit-Remaining` | Requests remaining in the current API-key window after authentication. | HTTP status code (`202` on success). Unique identifier for the created task. Use this to poll for results. The task type that was submitted. Initial status, always `"pending"`. Number of credits frozen for this task. ISO 8601 timestamp of when the task was created. The delivery format this task was created with — the value you sent, or `null` if you omitted it. `null` means `audio_url` points at the provider's own file, whose format is not fixed. ```json 202 theme={null} { "code": 202, "data": { "task_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "type": "music", "status": "pending", "credits_charged": 10, "created_at": "2025-01-15T10:30:00.000Z" } } ``` ## Code examples ```bash cURL theme={null} curl -X POST https://sunor.cc/api/v1/task \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "model": "suno", "task_type": "music", "input": { "gpt_description_prompt": "A cheerful acoustic guitar song about summer", "make_instrumental": false } }' ``` ```python Python theme={null} import requests response = requests.post( "https://sunor.cc/api/v1/task", headers={ "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, json={ "model": "suno", "task_type": "music", "input": { "gpt_description_prompt": "A cheerful acoustic guitar song about summer", "make_instrumental": False, }, }, ) data = response.json() task_id = data["data"]["task_id"] print(f"Task created: {task_id}") ``` ```javascript Node.js theme={null} const response = await fetch("https://sunor.cc/api/v1/task", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": "YOUR_API_KEY", }, body: JSON.stringify({ model: "suno", task_type: "music", input: { gpt_description_prompt: "A cheerful acoustic guitar song about summer", make_instrumental: false, }, }), }); const data = await response.json(); console.log("Task created:", data.data.task_id); ``` ## Errors Errors retain the numeric `code` and human-readable `message`, and also include a stable machine-readable `error_code`. See [Error Codes](/errors) for the full list. | Status | Representative `error_code` | Description | | ------ | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `400` | `invalid_json`, `invalid_request_body`, `missing_required_fields`, `invalid_model`, `unsupported_task_type`, `invalid_task_input` | Malformed request or invalid model/task input | | `401` | `missing_api_key`, `invalid_api_key` | Missing or invalid API key | | `402` | `insufficient_credits` | Insufficient credits | | `403` | `account_suspended` | Account cannot create tasks | | `429` | `rate_limited` | Rate limit exceeded | | `500` | `internal_error` | Internal server error | | `502` | `upstream_provider_error` | Upstream model provider error | Task creation does not currently support `Idempotency-Key`. If a network failure or uncertain `5xx` leaves the create outcome unknown, do not automatically submit the same payload again. If a `task_id` was returned, poll that task instead. # Get Balance Source: https://docs.sunor.cc/api-reference/get-balance GET https://sunor.cc/api/v1/account/balance Check your current credit balance # Get Balance Retrieve your current credit balance, including available credits and credits frozen by in-progress tasks. ## Request ``` GET /api/v1/account/balance ``` ### Headers Your API key. ## Response HTTP status code (`200` on success). Credits available to spend on new tasks. Credits reserved by tasks that are currently in progress. Total credits (`available` + `frozen`). ## Example response ```json 200 theme={null} { "code": 200, "data": { "available": 950, "frozen": 10, "total": 960 } } ``` ## Code examples ```bash cURL theme={null} curl https://sunor.cc/api/v1/account/balance \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://sunor.cc/api/v1/account/balance", headers={"x-api-key": "YOUR_API_KEY"}, ) balance = response.json()["data"] print(f"Available: {balance['available']} credits") print(f"Frozen: {balance['frozen']} credits") print(f"Total: {balance['total']} credits") ``` ```javascript Node.js theme={null} const response = await fetch("https://sunor.cc/api/v1/account/balance", { headers: { "x-api-key": "YOUR_API_KEY" }, }); const { data } = await response.json(); console.log(`Available: ${data.available} credits`); console.log(`Frozen: ${data.frozen} credits`); console.log(`Total: ${data.total} credits`); ``` ## Errors | Status | Description | | ------ | -------------------------- | | `401` | Missing or invalid API key | | `500` | Internal server error | # Get Task Source: https://docs.sunor.cc/api-reference/get-task GET https://sunor.cc/api/v1/task/{taskId} Retrieve the status and output of a task # Get Task Retrieve the current status, input, and output of a previously created task. If the task is still in progress, sunor will live-poll the upstream provider and return the latest status. ## Request ``` GET /api/v1/task/{taskId} ``` ### Path parameters The task ID returned from the [Create Task](/api-reference/create-task) endpoint. ### Headers Your API key. ## Response HTTP status code (`200` on success). The unique task identifier. The model used (e.g., `"suno"`). The task type (`"music"`, `"lyrics"`, `"upload"`, or `"concat"`). Current task status. One of: `"pending"`, `"running"`, `"success"`, `"failure"`, `"timeout"`. Credits charged (or frozen) for this task. The original input parameters submitted with the task. The delivery format this task was created with — the value sent on the create request, or `null` if it was omitted. `null` means `audio_url` points at the provider's own file, whose format is not fixed. See [Audio format](#audio-format). The task output. `null` while the task is still processing. Contains model-specific results on completion. Human-readable message if the task failed. `null` otherwise. **Prose — do not branch on it.** It may be reworded at any time; branch on `error_code`. Stable machine-readable classification of a failed task. `null` unless `status` is `failure` or `timeout`. One of `content_moderation`, `invalid_task_input`, `rate_limited`, `upstream_provider_error` — see [Error Codes](/errors#failed-tasks). `false` means resending the same request can never succeed — change the input, or surface it to a person. `null` unless the task failed. Seconds to wait before retrying, when the provider states a concrete wait. `null` otherwise. ISO 8601 timestamp of task creation. ISO 8601 timestamp of task completion. `null` if not yet completed. ### Status values | Status | Description | | --------- | ------------------------------------------------------------------------------------------------------------- | | `pending` | Task has been submitted and is waiting to be processed | | `running` | Task is actively being processed by the upstream provider | | `success` | Task completed successfully. Output is available | | `failure` | Task failed. Branch on `error_code` / `retryable`; `error` is the human-readable reason. Credits are refunded | | `timeout` | Task timed out. Credits are refunded | ## Audio URL lifetime **`audio_url` is valid for 7 days after the task completes.** After that it returns `404` permanently. If you need the audio for longer, download and store it yourself. Cover art (`image_url`, `image_large_url`) is served by the model provider and is not covered by this window. ## Contract The fields listed on this page are the response contract, and they hold across provider changes: every one of them has been present on every clip we have returned. `output.result` also carries additional fields from the upstream model provider. Those are passed through as-is. They are not part of the contract, they differ between providers, and they may appear or disappear without notice — build against the documented fields. That last clause is not hypothetical. Some of those fields exist on one provider and not at all on another: routing a request differently makes them vanish from every clip, with no other change on our side and no announcement. Two further things are worth knowing about them: * Where such a field lists alternate URLs for the same audio, and the clip's own `audio_url` is one we serve, we **remove any entry that still points at a provider's own origin** and remove the field entirely when no entry survives. What is left is served from our domain under the same retention as `audio_url`. * **For any other clip that field is passed through untouched**, provider origins included — a clip we could not store keeps upstream's list verbatim, because editing a response we are not otherwise the source of would be worse than leaving it alone. Those URLs expire on the provider's schedule, not ours, and some of them are streams a normal client cannot play. `audio_url` is the field to use; treat the alternates as informational at best. If you have built on an undocumented field, treat it as something that can disappear between two consecutive requests. ## Audio format By default `audio_url` points at the file exactly as the provider produced it. **That format is not fixed, and it has changed more than once.** As of 2026-09-06 it is MP3, served with a `.mp3` extension. Between 2026-08-29 and 2026-09-05 it was Opus in an MP4 container, served with an `.m4a` extension. It can change for two different reasons, and the second one is ours: 1. The provider changes what it produces — that is what happened on both dates above. 2. **We route your request to a different provider.** We run more than one, they do not all produce the same format, and we will move traffic between them to keep the service up. When that happens the default format changes with it, without notice and without any change on your side. **If your pipeline needs one specific format, pin it.** Send `audio_format: "mp3"` when you create the task and `audio_url` will point at MP3 — we convert on our side when the source is something else, and skip the conversion when it is already MP3. Leaving the field out keeps today's behaviour exactly: you get the provider's own file, in whatever format that is. One case is worth knowing about. **If the conversion fails we do not fail the task** — you get our copy of the source file instead: our URL, our retention, the source format. A conversion that costs you the recording you paid for would be a bad trade for an optional convenience. The response still echoes back the `audio_format` you asked for, because that field records the request, not the outcome — so **`Content-Type` remains the only reliable statement of what a file actually is.** Every response carries it, and the paragraphs below explain why it is worth reading. Two more things follow, and both are worth building around rather than checking once: * **Read the format from the response's `Content-Type`.** Not from the URL's extension, and not from what the provider was serving the last time you looked. An `.m4a` extension in particular does not imply AAC. * **Do not assume every container decodes on every platform.** Apple's CoreAudio — the API every iOS and macOS app sits on — cannot decode Opus in an MP4 container at all. If you target Apple platforms, branch on `Content-Type` and convert on your side when you receive something it cannot read. ## Output format The `output` field structure depends on the task type. All outputs share a common wrapper: The task type (`"music"`, `"lyrics"`, `"upload"`, or `"concat"`). Internal processing status: `"queued"`, `"processing"`, `"completed"`, or `"failed"`. Processing progress (e.g., `"50%"`, `"100%"`). Reason for failure, if any. The task-specific result. Shape varies by task type (see below). ### Output by task type `result` is an **array of clip objects**. Each music generation typically returns one or more clip variations. | Field | Type | Description | | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | `string` | Unique clip identifier. Use this for continuation or concat tasks | | `audio_url` | `string` | URL to the generated audio file. Derive the format from the response's `Content-Type` rather than assuming one. See [Audio URL lifetime](#audio-url-lifetime) | | `image_url` | `string` | URL to the generated cover art | | `title` | `string` | Generated or provided song title | | `metadata` | `object` | Additional info: `duration` (seconds), `tags`, `prompt`, etc. | ```json theme={null} "output": { "task_type": "music", "status": "completed", "progress": "100%", "fail_reason": null, "result": [ { "id": "clip-id-1", "audio_url": "https://audio.sunor.cc/audio/6fdfdf84-43d5-4a63-90c4-340e678739b2/810509c7d9873364.mp3", "image_url": "https://cdn2.suno.ai/image_clip-id-1.jpeg", "title": "Summer Days", "metadata": { "duration": 120, "tags": "acoustic, cheerful, summer" } }, { "id": "clip-id-2", "audio_url": "https://audio.sunor.cc/audio/6fdfdf84-43d5-4a63-90c4-340e678739b2/72683fa7bd9b4723.mp3", "image_url": "https://cdn2.suno.ai/image_clip-id-2.jpeg", "title": "Summer Days", "metadata": { "duration": 118, "tags": "acoustic, cheerful, summer" } } ] } ``` `result` is an **object** containing the generated lyrics and title. | Field | Type | Description | | ------- | -------- | ------------------------------------------------------------------ | | `title` | `string` | Generated song title | | `text` | `string` | Generated lyrics with structure tags (`[Verse]`, `[Chorus]`, etc.) | ```json theme={null} "output": { "task_type": "lyrics", "status": "completed", "progress": "100%", "fail_reason": null, "result": { "title": "Ocean Sunset", "text": "[Verse]\nWaves are crashing on the shore\nGolden light I can't ignore\n\n[Chorus]\nAt the edge of the sea\nYou and me, finally free" } } ``` `result` is an **array** containing the uploaded clip information. | Field | Type | Description | | ----------- | -------- | ------------------------------------------------------------------------ | | `id` | `string` | Clip ID for the uploaded audio. Use this in other tasks | | `audio_url` | `string` | URL to the uploaded audio. See [Audio URL lifetime](#audio-url-lifetime) | ```json theme={null} "output": { "task_type": "upload", "status": "completed", "progress": "100%", "fail_reason": null, "result": [ { "id": "uploaded-clip-id", "audio_url": "https://audio.sunor.cc/audio/6fdfdf84-43d5-4a63-90c4-340e678739b2/a256638015d2e5f4.mp3" } ] } ``` `result` is an **array** containing the concatenated clip. | Field | Type | Description | | ----------- | -------- | --------------------------------------------------------------------------------- | | `id` | `string` | Clip ID for the concatenated audio | | `audio_url` | `string` | URL to the concatenated audio file. See [Audio URL lifetime](#audio-url-lifetime) | ```json theme={null} "output": { "task_type": "concat", "status": "completed", "progress": "100%", "fail_reason": null, "result": [ { "id": "concat-clip-id", "audio_url": "https://audio.sunor.cc/audio/6fdfdf84-43d5-4a63-90c4-340e678739b2/b5cdf870fc295ff7.mp3" } ] } ``` *** ## Example responses ### Task in progress ```json 200 theme={null} { "code": 200, "data": { "task_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "model": "suno", "type": "music", "status": "running", "credits_cost": 10, "input": { "gpt_description_prompt": "A cheerful acoustic guitar song about summer", "make_instrumental": false }, "output": { "task_type": "music", "status": "processing", "progress": "50%", "fail_reason": null, "result": null }, "error": null, "created_at": "2025-01-15T10:30:00.000Z", "completed_at": null } } ``` ### Task completed (music) ```json 200 theme={null} { "code": 200, "data": { "task_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "model": "suno", "type": "music", "status": "success", "credits_cost": 10, "input": { "gpt_description_prompt": "A cheerful acoustic guitar song about summer", "make_instrumental": false }, "output": { "task_type": "music", "status": "completed", "progress": "100%", "fail_reason": null, "result": [ { "id": "clip-id-1", "audio_url": "https://audio.sunor.cc/audio/6fdfdf84-43d5-4a63-90c4-340e678739b2/810509c7d9873364.mp3", "image_url": "https://cdn2.suno.ai/image_clip-id-1.jpeg", "title": "Summer Days", "metadata": { "duration": 120, "tags": "acoustic, cheerful, summer" } } ] }, "error": null, "created_at": "2025-01-15T10:30:00.000Z", "completed_at": "2025-01-15T10:32:15.000Z" } } ``` ### Task failed ```json 200 theme={null} { "code": 200, "data": { "task_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "model": "suno", "type": "music", "status": "failure", "credits_cost": 10, "input": { "gpt_description_prompt": "A cheerful acoustic guitar song about summer" }, "output": { "task_type": "music", "status": "failed", "progress": null, "fail_reason": "Generation failed and your credits were refunded. Please try again; if this keeps happening, contact support@sunor.cc.", "result": null }, "error": "Generation failed and your credits were refunded. Please try again; if this keeps happening, contact support@sunor.cc.", "error_code": "upstream_provider_error", "retryable": true, "retry_after_seconds": null, "created_at": "2025-01-15T10:30:00.000Z", "completed_at": "2025-01-15T10:30:45.000Z" } } ``` ## Polling strategy Tasks typically take **30 seconds to 5 minutes** to complete, depending on the task type and upstream provider load. Poll the Get Task endpoint every **5-10 seconds** until the status is `"success"`, `"failure"`, or `"timeout"`. Avoid polling more frequently than once per second. ### Polling example ```python Python theme={null} import time import requests API_KEY = "YOUR_API_KEY" TASK_ID = "f47ac10b-58cc-4372-a567-0e02b2c3d479" while True: response = requests.get( f"https://sunor.cc/api/v1/task/{TASK_ID}", headers={"x-api-key": API_KEY}, ) data = response.json()["data"] status = data["status"] print(f"Status: {status}") if status in ("success", "failure", "timeout"): print("Result:", data["output"]) break time.sleep(5) ``` ```javascript Node.js theme={null} const API_KEY = "YOUR_API_KEY"; const TASK_ID = "f47ac10b-58cc-4372-a567-0e02b2c3d479"; async function pollTask() { while (true) { const response = await fetch( `https://sunor.cc/api/v1/task/${TASK_ID}`, { headers: { "x-api-key": API_KEY } } ); const { data } = await response.json(); console.log("Status:", data.status); if (["success", "failure", "timeout"].includes(data.status)) { console.log("Result:", data.output); break; } await new Promise((r) => setTimeout(r, 5000)); } } pollTask(); ``` ## Errors | Status | Description | | ------ | ----------------------------------------------------------- | | `401` | Missing or invalid API key | | `404` | Task not found (invalid ID or task belongs to another user) | | `429` | Rate limit exceeded | | `500` | Internal server error | # Get Usage Source: https://docs.sunor.cc/api-reference/get-usage GET https://sunor.cc/api/v1/account/usage Retrieve your account usage statistics # Get Usage Retrieve aggregate usage statistics for your account, including total tasks created and credits consumed. ## Request ``` GET /api/v1/account/usage ``` ### Headers Your API key. ## Response HTTP status code (`200` on success). Total number of tasks you have created (all statuses). Total credits consumed by all tasks. Total credits added to your account via top-ups. ## Example response ```json 200 theme={null} { "code": 200, "data": { "tasks_created": 42, "credits_used": 350, "credits_topped_up": 1000 } } ``` ## Code examples ```bash cURL theme={null} curl https://sunor.cc/api/v1/account/usage \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://sunor.cc/api/v1/account/usage", headers={"x-api-key": "YOUR_API_KEY"}, ) usage = response.json()["data"] print(f"Tasks created: {usage['tasks_created']}") print(f"Credits used: {usage['credits_used']}") print(f"Credits topped up: {usage['credits_topped_up']}") ``` ```javascript Node.js theme={null} const response = await fetch("https://sunor.cc/api/v1/account/usage", { headers: { "x-api-key": "YOUR_API_KEY" }, }); const { data } = await response.json(); console.log(`Tasks created: ${data.tasks_created}`); console.log(`Credits used: ${data.credits_used}`); console.log(`Credits topped up: ${data.credits_topped_up}`); ``` ## Errors | Status | Description | | ------ | -------------------------- | | `401` | Missing or invalid API key | | `500` | Internal server error | # Authentication Source: https://docs.sunor.cc/authentication Authenticate your Sunor API requests with an x-api-key header. Get your API key from the Sunor dashboard. All API requests require authentication via an API key passed in the `x-api-key` HTTP header. ## Getting your API key 1. Sign in to your [sunor dashboard](https://sunor.cc/dashboard). 2. Navigate to [API Keys](https://sunor.cc/dashboard/api-keys). 3. Click **Create API Key** and give it a name. 4. Copy the key immediately -- it will only be shown once. ## Using your API key Include the `x-api-key` header in every request: ```bash cURL theme={null} curl -X GET https://sunor.cc/api/v1/account/balance \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://sunor.cc/api/v1/account/balance", headers={"x-api-key": "YOUR_API_KEY"}, ) print(response.json()) ``` ```javascript Node.js theme={null} const response = await fetch("https://sunor.cc/api/v1/account/balance", { headers: { "x-api-key": "YOUR_API_KEY" }, }); const data = await response.json(); console.log(data); ``` ## Error responses | Status | Meaning | | ------ | ------------------------------------- | | `401` | Missing or invalid `x-api-key` header | Example error response: ```json theme={null} { "code": 401, "error_code": "missing_api_key", "message": "Missing x-api-key header" } ``` Every public API response includes an `X-Request-Id` header for correlation. You may send your own safe `X-Request-Id` value, or let Sunor generate one. Authentication failures occur before an API key's quota can be read, so an early `401` can omit `X-RateLimit-Remaining`. ## Security tips **Never expose your API key in client-side code.** API keys should only be used in server-side applications or secure environments. * Store your API key in environment variables, not in source code. * Rotate keys periodically from the [API Keys](https://sunor.cc/dashboard/api-keys) page. * If you suspect a key has been compromised, delete it immediately and create a new one. * Each key is tied to your account -- anyone with the key can use your credits. # Automatic Top-Up Source: https://docs.sunor.cc/billing/automatic-top-up Configure optional Stripe Automatic Top-Up in Sunor with a balance threshold, recharge amount, monthly cap, explicit authorization, and payment recovery. Automatic Top-Up can help keep an eligible Sunor account funded when its available credit balance runs low. It remains off until the user explicitly enables and authorizes it. Automatic Top-Up is available only where it is shown in your Billing page. It uses a saved Stripe card, remains off by default, and does not apply to cryptocurrency payments. ## Before you begin You need: * a Sunor account for which Automatic Top-Up is available; * a Stripe card saved in [Dashboard > Billing](https://sunor.cc/dashboard/billing); * an available-balance threshold of at least **50 credits**; * a whole-dollar recharge amount from **$10 to $1,000**; * a whole-dollar monthly cap from **$10 to $10,000**, at least as large as the recharge amount. Sunor stores only the Stripe references needed to use the saved payment method. Full card details remain with Stripe. ## Configure Automatic Top-Up Open [Dashboard > Billing](https://sunor.cc/dashboard/billing), find **Payment method for Automatic Top-Up**, and save a card. Replacing or removing the card is also managed from this section. In **Automatic Top-Up**, enter the available-credit threshold that should trigger a recharge. Choose the USD amount for each automatic recharge and the maximum total Automatic Top-Up amount you authorize for a calendar month. Select **Enable Automatic Top-Up**, review the authorization text, actively agree to it, and save the settings. The saved status and saved configuration are shown separately from unsaved form changes. Discard or save changes before relying on the displayed settings. ## What happens when the balance is low When available credits fall below your saved threshold, Sunor can charge the saved Stripe card for the recharge amount you chose. Credits are added only after the payment succeeds. Each completed Automatic Top-Up appears in: * **Automatic Top-Up history**; * **Credit History**; * **Invoices**, after you save the required billing information. Sunor also applies service-side cooldowns, limits, and failure protections. Those controls can prevent or pause another charge even when your configured threshold has been crossed. ## Changing or disabling the settings You can disable Automatic Top-Up from Billing at any time. Disabling it stops future automatic purchase attempts but does not refund credits already purchased. You must authorize again when you: * enable Automatic Top-Up; * change the threshold; * change the recharge amount; * change the monthly cap. Removing the saved card also prevents future Automatic Top-Up charges until a valid card is saved and the feature is enabled again. ## Payment verification and failures A bank can require additional card verification for an automatic payment. When that happens: 1. the payment appears in Automatic Top-Up history with an action-required state; 2. select **Complete verification**; 3. finish the one-time Stripe verification; 4. refresh Billing to check the final payment result. Automatic Top-Up can be paused after repeated card failures or when a safety limit is reached. Review the message in Billing, update the saved card if needed, and save the settings again to re-enable it. Automatic credit purchases are prepaid and subject to the same [Terms of Service](https://sunor.cc/terms) and [Refund Policy](https://sunor.cc/refund) as manual credit purchases. ## Related pages * [Credits](/credits) * [Billing Information and PDF Invoices](/billing/invoices) * [Error Codes](/errors) # Billing Information and PDF Invoices Source: https://docs.sunor.cc/billing/invoices Save customer-supplied billing information and download private PDF invoices for completed Sunor card, crypto, and Automatic Top-Up purchases. Sunor lets you save billing information and download a PDF invoice for each completed paid credit purchase. ## Save billing information Open [Dashboard > Billing](https://sunor.cc/dashboard/billing) and complete the **Billing information** section. | Field | Requirement | | --------------------- | ----------- | | Company or legal name | Required | | VAT / Tax ID | Optional | | Address line 1 | Required | | Address line 2 | Optional | | City | Required | | Postal code | Optional | | Country | Required | This information is customer-supplied and appears in the **Bill to** section of the PDF. The invoice generated at download time uses the billing information currently saved in your account. Sunor does not calculate or validate VAT, GST, Tax ID, or other tax treatment from the billing information you enter. Confirm your accounting, reimbursement, and tax requirements under your own organization and jurisdiction. ## Which purchases have invoices An invoice is available only after a credit purchase is recorded as paid. Supported completed purchases include: * card payments processed through Stripe; * cryptocurrency payments processed through OxaPay; * completed Automatic Top-Up payments where that feature is available. Pending, failed, expired, or otherwise unpaid payments cannot produce an invoice. Bonus credits, signup credits, administrative grants, and other non-payment credit entries are not separate paid purchases. ## Download a PDF invoice Sign in and go to [Dashboard > Billing](https://sunor.cc/dashboard/billing). Complete the required Billing information fields. Invoice download remains disabled until this information is saved. In **Invoices**, locate the completed Card, Crypto, or Automatic Top-Up purchase. Select **Download PDF**. You can update your billing information and download the invoice again if the Bill to details need correction. The PDF includes the invoice number, paid date, payment type, purchased credits, amount paid in USD, merchant information, and your saved Bill to information. ## Privacy and access Invoice data belongs to the signed-in account: * the user invoice list contains only that user's paid purchases; * the PDF download checks ownership on the server; * invoice PDFs are returned as private, non-cacheable downloads; * full card details remain with Stripe and are not included in the PDF. Do not share an invoice URL or downloaded PDF with people who should not have access to your billing records. ## Troubleshooting ### The purchase is not listed Confirm that the payment has completed. A purchase does not enter the invoice list while it is pending or failed. ### Download PDF is disabled Save all required Billing information fields, then return to the Invoices section. ### The Bill to details are wrong Update Billing information and download the PDF again. The generated document uses the current saved billing profile. ### Automatic Top-Up needs card verification Complete the verification from **Automatic Top-Up history** first. The payment can be invoiced after it succeeds. ## Related pages * [Credits](/credits) * [Automatic Top-Up](/billing/automatic-top-up) * [Refund Policy](https://sunor.cc/refund) # Changelog Source: https://docs.sunor.cc/changelog What's new in the sunor API # Changelog ## September 2026 ### Suno V6 sunor now generates with **Suno V6**, Suno's current model, and `model_version` defaults to `"v6"`. Suno retired every earlier model on September 9, 2026. Requests that still send `model_version: "v5.5"` or `"v5"` keep working and are generated with V6 — no code change is required. Any other value is rejected with a `400` that lists the accepted values. See [Suno Model → Model version](/models/suno#model-version). ## August 2026 ### Automatic Top-Up Eligible accounts can now configure optional Automatic Top-Up from Billing: * save a Stripe card for automatic purchases; * set an available-credit threshold, recharge amount, and monthly cap; * explicitly authorize the recurring charges before enabling the feature; * complete additional bank verification from Automatic Top-Up history when required; * disable the feature or update the card and settings from Billing. Automatic Top-Up remains unavailable unless it is shown for the account, stays off until explicitly enabled and authorized, and does not apply to crypto payments. See [Automatic Top-Up](/billing/automatic-top-up). ### Self-Service PDF Invoices Users can now save customer-supplied billing information and download a PDF invoice for each completed paid credit purchase: * card payments through Stripe; * cryptocurrency payments through OxaPay; * completed Automatic Top-Up payments. Only paid purchases can be invoiced. Sunor does not calculate or validate VAT/GST from the saved billing information. See [Billing Information and PDF Invoices](/billing/invoices). ### Card and Crypto Top-Ups Self-service credit purchases now support card payment through Stripe as well as cryptocurrency through OxaPay. The current self-service range is $10 to $1,000 per purchase. See [Credits](/credits#topping-up-credits). ## June 2026 ### Suno Audio Upload → Full Song Uploaded audio can now be extended into a full song. Upload your audio (`task_type: upload`), then pass the returned clip `id` as `continue_clip_id` in a `music` task — see [Audio upload](/models/suno#audio-upload). This also fixes continuation requests, which previously failed upstream. Generating a new song in a *different* style from an uploaded reference (an "audio cover") is not yet supported. ### Suno Custom Instrumental Custom mode now supports **style-driven instrumentals**. Provide `tags` and set `make_instrumental: true` with no `prompt` to generate an instrumental from style tags alone — see [Create Task → Custom Mode](/api-reference/create-task). A music request with no `prompt`, `tags`, or `gpt_description_prompt` is now rejected with a clear `400` instead of a generic upstream error. ### Volume Top-up Rewards Larger top-ups now earn **bonus credits** automatically: * **+5%** on $500–$999, **+10%** on $1,000–$2,999, **+12%** on \$3,000+. * The bonus is granted when payment is confirmed (e.g. a \$500 top-up → 52,500 credits). See [Credits](/credits#volume-bonus). * Self-serve top-up remains capped at $1,000; for $1,000+ volume pricing, [contact us](https://t.me/+Wz-V9IBpSY5lMzE1). ## May 2026 ### Udio Input Validation The Udio task input is now validated at the API layer for clearer errors: * Submitting Udio tasks with Suno-style fields (`gpt_description_prompt`, `make_instrumental`) now returns **`400 Bad Request`** with a message that names the right field — instead of the previous generic `502 "Upstream provider error"`. * The validation message links to the [Udio Model](/models/udio) docs so first-time users can correct their request on the next try. * No change to existing Udio integrations that already use the `prompt` field. ### Udio Model Support sunor now supports the Udio AI music model alongside Suno. Use `"model": "udio"` in your task request. * Task type: `"music"` only (regular mode) — see [Udio Model](/models/udio) for the full reference. * Pricing: **5 credits per generation** (\$0.05). * \~2 audio tracks returned per call (same as Suno's music task). Lyrics-only generation, audio upload, and clip concatenation are not yet supported for Udio. Use the [Suno model](/models/suno) for those. ## April 2026 ### Suno V5.5 Support sunor now uses **Suno V5.5** (`chirp-fenix`) as the default model for music generation. V5.5 brings improved audio quality, better vocals, and more accurate style adherence. * **No API changes required** -- your existing code works as-is * **Same pricing** -- 10 credits per music generation * The model version is set automatically; no need to specify it in requests ### Contact & Community * Added [Telegram group](https://t.me/+Wz-V9IBpSY5lMzE1) for community support * Email support available at [support@sunor.cc](mailto:support@sunor.cc) *** ## February 2026 ### Launch * sunor API launched with Suno V5 (`chirp-crow`) support * Task types: music, lyrics, upload, concat * Pay-as-you-go credits system * Crypto payments via OxaPay # Credits Source: https://docs.sunor.cc/credits Pay-as-you-go credits for the sunor API. Suno music 10 credits ($0.10), Udio music 5 credits ($0.05), Suno lyrics 5 credits, no subscription. sunor uses a **pay-as-you-go credits system**. You top up credits and they are consumed when you create tasks. ## Credit value **1 credit = \$0.01 USD** For example, 1000 credits = \$10.00 USD. ## Pricing by model and task type ### Suno | Task Type | Credits | USD Equivalent | Description | | --------- | ------- | -------------- | ------------------------------------------- | | `music` | 10 | \$0.10 | Generate a music clip from a prompt | | `lyrics` | 5 | \$0.05 | Generate song lyrics from a prompt | | `upload` | 1 | \$0.01 | Upload an audio file for use with AI models | | `concat` | 5 | \$0.05 | Concatenate two clips into one | ### Udio | Task Type | Credits | USD Equivalent | Description | | --------- | ------- | -------------- | ---------------------------------------- | | `music` | 5 | \$0.05 | Generate a music clip via the Udio model | ## How billing works When you submit a task, sunor follows a **freeze-then-settle** billing model: When a task is submitted, the required credits are immediately frozen (reserved) from your available balance. This prevents over-spending. The task is sent to the upstream AI provider and processed asynchronously. * **Success**: Frozen credits are permanently deducted from your balance. * **Failure**: Frozen credits are automatically refunded to your available balance. ## Topping up credits 1. Go to [Dashboard > Billing](https://sunor.cc/dashboard/billing). 2. Enter a whole-dollar amount from **$10 to $1,000**. 3. Choose **Card** to pay through Stripe or **Crypto (USDT)** to pay through OxaPay. 4. Complete the payment. Credits are added after Sunor records the payment as successful. Where Automatic Top-Up is available for an eligible account, it remains off until the user explicitly enables and authorizes it. See [Automatic Top-Up](/billing/automatic-top-up). ## Volume bonus Larger top-ups earn **bonus credits** on top of the credits you pay for: | Top-up amount | Bonus | | --------------- | -------- | | $500 – $999 | **+5%** | | $1,000 – $2,999 | **+10%** | | \$3,000+ | **+12%** | The bonus is granted automatically when your payment is confirmed. For example, a \$500 top-up gives 50,000 credits + 2,500 bonus = **52,500 credits**. Self-serve top-up is capped at $1,000. For $1,000+ volume pricing (including the +12% tier), [contact us on Telegram](https://t.me/+Wz-V9IBpSY5lMzE1) or email [support@sunor.cc](mailto:support@sunor.cc). ## Billing information and invoices Save your company or legal name, optional VAT / Tax ID, and billing address in [Dashboard > Billing](https://sunor.cc/dashboard/billing). You can then download a PDF invoice for each completed paid credit purchase, including card, crypto, and completed Automatic Top-Up purchases. See [Billing Information and PDF Invoices](/billing/invoices) for the supported fields, payment-state requirements, privacy boundary, and tax disclaimer. ## Credit expiry Credits expire **12 months** from the date of each top-up. Unused credits after this period are forfeited. ## Checking your balance Use the [Get Balance](/api-reference/get-balance) endpoint to check your current credits: ```bash theme={null} curl https://sunor.cc/api/v1/account/balance \ -H "x-api-key: YOUR_API_KEY" ``` Response: ```json theme={null} { "code": 200, "data": { "available": 950, "frozen": 10, "total": 960 } } ``` | Field | Description | | ----------- | ------------------------------------- | | `available` | Credits available to spend | | `frozen` | Credits reserved by in-progress tasks | | `total` | `available` + `frozen` | ## Failed tasks If a task fails for any reason (upstream provider error, timeout, etc.), the frozen credits are **automatically refunded** to your available balance. You are never charged for failed tasks. # Error Codes Source: https://docs.sunor.cc/errors HTTP error codes and error response format ## Error response format All error responses follow this structure: ```json theme={null} { "code": 400, "error_code": "missing_required_fields", "message": "Missing required fields: model, task_type, input" } ``` | Field | Type | Description | | ------------ | -------- | ----------------------------------------------------------- | | `code` | `number` | The HTTP status code | | `error_code` | `string` | Stable machine-readable classification | | `message` | `string` | A human-readable description that may change without notice | Branch on `error_code`, not by parsing `message`. Every public API response also includes an `X-Request-Id` header. Record that identifier with your error logs and include it when contacting support. Common stable values include: | `error_code` | Meaning | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `invalid_json` | The request body is not valid JSON | | `invalid_request_body` | The decoded body is not a JSON object | | `missing_required_fields` | `model`, `task_type`, or `input` is missing | | `invalid_model` | The requested model is unsupported | | `unsupported_task_type` | The model does not support the requested task type | | `invalid_task_input` | A model- or task-specific input field is invalid | | `invalid_audio_format` | `audio_format` was sent with an unsupported value | | `audio_format_unavailable` | MP3 conversion is temporarily switched off — omit `audio_format` and you will receive the provider's original file | | `missing_api_key` / `invalid_api_key` | Authentication failed | | `insufficient_credits` | Available credits do not cover the task | | `account_suspended` | The authenticated account cannot create tasks | | `rate_limited` | The API key exceeded its current quota | | `task_not_found` | The task does not exist or belongs to another account | | `upstream_provider_error` | An upstream model provider failed | | `internal_error` | Sunor encountered an unexpected error | ## Failed tasks The codes above describe a **request** that failed. A request can also succeed — `POST /v1/task` returns `200`, credits are held, the task is created — and the **task** fail later. Poll `GET /v1/task/{task_id}` and that failure arrives on the task itself: ```json theme={null} { "code": 200, "data": { "task_id": "0f0c8b2e-...", "status": "failure", "error": "Your style description is too long - please shorten it to 1000 characters or fewer to continue.", "error_code": "invalid_task_input", "retryable": false, "retry_after_seconds": null } } ``` The same rule applies: **branch on `error_code` and `retryable`, not on `error`.** `error` is prose and may be reworded at any time. | Field | Type | Description | | --------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `error_code` | `string \| null` | One of the values below. `null` unless `status` is `failure` or `timeout` | | `retryable` | `boolean \| null` | `false` means resending the same request can never succeed — change the input, or surface it to a person | | `retry_after_seconds` | `integer \| null` | A wait we can state at this level: the repeat-submission window, and the 300 seconds our own "Service temporarily unavailable" failure asks for. `null` otherwise, which does not always mean there is no wait: some providers put one inside `output` instead, so check there before falling back to your own backoff | | `error_code` | When | `retryable` | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `content_moderation` | Lyrics, prompt, tags or uploaded audio were rejected on content grounds — copyrighted material, a named artist, a producer tag | usually `false`; `true` for the one verdict the provider is known to reverse on identical audio. Read the field | | `invalid_task_input` | An input field was rejected: style description over the length limit, uploaded audio too short or unreadable, Suno-only fields sent to Udio | `false` | | `rate_limited` | The provider is refusing the request for now: either a repeat submission of the same input too soon (`retry_after_seconds` carries the wait), or its own quota is spent for the period (`retry_after_seconds` is `null` — the reset is the provider's own boundary and we will not guess it) | `true` | | `upstream_provider_error` | The provider timed out, was unavailable, or failed for a reason we cannot attribute to your request | usually `true` — but `false` when the provider itself states the request will not succeed on retry. Always read the field; do not infer it from the code | An error we have not classified reports `upstream_provider_error` with `retryable: true`. We never report `invalid_task_input` for an unrecognised failure — that would send you to debug a request that was fine. ### Retrying correctly `retryable: false` means the input must change. Retrying it unchanged consumes a credit hold and a provider round trip each time, and fails identically: ```js theme={null} const task = await pollTask(taskId); // `timeout` is a terminal failure too, and it carries the same three fields. // Branching on "failure" alone silently drops it — measured over 180 days, // timeouts were 14.6% of all terminal states (1,335 of 9,144). if (task.status === "failure" || task.status === "timeout") { if (!task.retryable) { // Nothing to wait for — the request itself has to change. throw new TaskInputError(task.error_code, task.error); } // Bound your retries. `retryable: true` means an attempt *may* succeed, not // that it will: a spent provider quota reports it with no wait attached, and // an unclassified failure reports it because we would rather you retried than // gave up on a task that was fine. if (attempt >= MAX_ATTEMPTS) throw new TaskFailedError(task.error_code, task.error); await sleep(backoff(attempt, task.retry_after_seconds)); return retry(attempt + 1); } ``` Credits are refunded on every failure, whatever the code. *** ## Error codes ### 400 — Bad Request The request body is malformed or missing required fields. **Common causes**: * Missing `model`, `task_type`, or `input` in the request body * Unknown or unsupported `task_type` * Invalid input parameters for the selected task type ```json theme={null} { "code": 400, "error_code": "missing_required_fields", "message": "Missing required fields: model, task_type, input" } ``` ### 401 — Unauthorized The API key is missing, invalid, or has been revoked. **Common causes**: * No `x-api-key` header in the request * API key does not exist or has been deleted * API key has been disabled ```json theme={null} { "code": 401, "error_code": "invalid_api_key", "message": "Invalid API key" } ``` ### 402 — Payment Required Your account does not have enough available credits to cover the task cost. **How to fix**: [Top up credits](https://sunor.cc/dashboard/billing) in your dashboard. ```json theme={null} { "code": 402, "error_code": "insufficient_credits", "message": "Insufficient credits: available 3, required 10" } ``` ### 403 — Forbidden The authenticated account is not currently allowed to create a task. ```json theme={null} { "code": 403, "error_code": "account_suspended", "message": "Account suspended" } ``` ### 404 — Not Found The requested resource does not exist. **Common causes**: * Invalid task ID * Task belongs to a different user ```json theme={null} { "code": 404, "error_code": "task_not_found", "message": "Task not found" } ``` ### 429 — Too Many Requests You have exceeded the [rate limit](/rate-limits). **How to fix**: Wait for the duration in `retry_after_seconds` (or the `Retry-After` header) before retrying. ```json theme={null} { "code": 429, "error_code": "rate_limited", "message": "Rate limit exceeded.", "retry_after_seconds": 60 } ``` ### 500 — Internal Server Error An unexpected error occurred on the server. **What to do**: For read-only requests, retry after a short delay. For `POST /api/v1/task`, do not automatically submit the same task again when the result is unknown; the current endpoint does not yet accept an `Idempotency-Key`. If you already received a `task_id`, poll that task instead. ```json theme={null} { "code": 500, "error_code": "internal_error", "message": "Internal server error" } ``` ### 502 — Bad Gateway The upstream AI provider returned an error or is temporarily unavailable. **What to do**: If the provider failure is explicit, the failed task follows the normal refund path. For an uncertain network or gateway result from `POST /api/v1/task`, do not blindly create another task. If you already received a `task_id`, poll that task instead. ```json theme={null} { "code": 502, "error_code": "upstream_provider_error", "message": "Upstream provider error" } ``` *** ## Handling errors Always check the HTTP status code before parsing the body. Follow `Retry-After` for `429` responses. Exponential backoff is appropriate for safe/read-only requests. For `POST /api/v1/task`, an uncertain `5xx` or network result should be treated as "create outcome unknown" rather than automatically submitted again. The API does not currently provide `Idempotency-Key` replay semantics. ### Example error handling ```python Python theme={null} import requests import time def create_task(api_key, payload, max_rate_limit_retries=3): rate_limit_attempts = 0 while True: response = requests.post( "https://sunor.cc/api/v1/task", headers={ "Content-Type": "application/json", "x-api-key": api_key, }, json=payload, ) if response.status_code == 202: return response.json()["data"] elif response.status_code == 429 and rate_limit_attempts < max_rate_limit_retries: retry_after = int(response.headers.get("Retry-After", 30)) time.sleep(retry_after) rate_limit_attempts += 1 elif response.status_code >= 500: raise RuntimeError( "Task creation outcome is unknown; do not submit the same " "payload again without an idempotency key." ) else: error = response.json() request_id = response.headers.get("X-Request-Id", "unknown") raise Exception( f"API error {error['error_code']} " f"(request {request_id}): {error['message']}" ) ``` ```javascript Node.js theme={null} async function createTask(apiKey, payload, maxRateLimitRetries = 3) { let rateLimitAttempts = 0; while (true) { const response = await fetch("https://sunor.cc/api/v1/task", { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": apiKey, }, body: JSON.stringify(payload), }); if (response.status === 202) { const json = await response.json(); return json.data; } else if ( response.status === 429 && rateLimitAttempts < maxRateLimitRetries ) { const retryAfter = parseInt(response.headers.get("Retry-After") || "30"); await new Promise((r) => setTimeout(r, retryAfter * 1000)); rateLimitAttempts += 1; } else if (response.status >= 500) { throw new Error( "Task creation outcome is unknown; do not submit the same payload " + "again without an idempotency key.", ); } else { const error = await response.json(); const requestId = response.headers.get("X-Request-Id") || "unknown"; throw new Error( `API error ${error.error_code} (request ${requestId}): ${error.message}`, ); } } } ``` # Complete Workflow Source: https://docs.sunor.cc/guides/complete-workflow End-to-end guide: create a task, poll for results, and use the output # Complete Workflow This guide walks through the full flow of generating music with sunor: authenticate, create a task, poll until it completes, and use the audio output. ## Prerequisites * A sunor account with an [API key](/authentication) * [Credits](/credits) in your account (music costs 10 credits) *** ## Step 1: Create a task Submit a music generation request: The example below uses **Suno**'s input shape (`gpt_description_prompt`, `make_instrumental`). **Udio** uses a different shape (`prompt`, `lyrics_type`) — see [/models/udio](/models/udio) before adapting this snippet to `"model": "udio"`. ```python Python theme={null} import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://sunor.cc/api/v1" response = requests.post( f"{BASE_URL}/task", headers={ "Content-Type": "application/json", "x-api-key": API_KEY, }, json={ "model": "suno", "task_type": "music", "input": { "gpt_description_prompt": "A chill lo-fi beat for studying, soft piano and vinyl crackle", "make_instrumental": True, }, }, ) task = response.json()["data"] task_id = task["task_id"] print(f"Task created: {task_id} (status: {task['status']})") ``` ```javascript Node.js theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://sunor.cc/api/v1"; const response = await fetch(`${BASE_URL}/task`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify({ model: "suno", task_type: "music", input: { gpt_description_prompt: "A chill lo-fi beat for studying, soft piano and vinyl crackle", make_instrumental: true, }, }), }); const { data: task } = await response.json(); console.log(`Task created: ${task.task_id} (status: ${task.status})`); ``` The response returns a `task_id` with status `"pending"`. Credits are frozen immediately. Before that point, Sunor validates the body and the selected model/task input. A `400` validation response happens before any credit freeze, task insert, or upstream-provider call. Record the response's `X-Request-Id` header so an individual attempt can be correlated across agent logs and Sunor support. If the create request ends with a network error or an uncertain `5xx` response, do not submit the same payload again automatically. The current create endpoint does not yet provide `Idempotency-Key` replay semantics. If you received a `task_id`, continue polling that task. *** ## Step 2: Poll for results Tasks take **30 seconds to 5 minutes** to complete. Poll every 5-10 seconds until the status is terminal (`success`, `failure`, or `timeout`). ```python Python theme={null} import time def wait_for_task(api_key, task_id, interval=5): """Poll until the task completes. Returns the task data.""" while True: response = requests.get( f"{BASE_URL}/task/{task_id}", headers={"x-api-key": api_key}, ) data = response.json()["data"] status = data["status"] print(f" Status: {status}") if status in ("success", "failure", "timeout"): return data time.sleep(interval) result = wait_for_task(API_KEY, task_id) ``` ```javascript Node.js theme={null} async function waitForTask(apiKey, taskId, interval = 5000) { while (true) { const response = await fetch(`${BASE_URL}/task/${taskId}`, { headers: { "x-api-key": apiKey }, }); const { data } = await response.json(); console.log(` Status: ${data.status}`); if (["success", "failure", "timeout"].includes(data.status)) { return data; } await new Promise((r) => setTimeout(r, interval)); } } const result = await waitForTask(API_KEY, task.task_id); ``` *** ## Step 3: Use the output On success, the `output.result` contains an array of generated clips: ```python Python theme={null} if result["status"] == "success": clips = result["output"]["result"] for clip in clips: print(f"Title: {clip['title']}") print(f"Audio: {clip['audio_url']}") print(f"Cover: {clip['image_url']}") print() else: print(f"Task failed: {result['error']}") ``` ```javascript Node.js theme={null} if (result.status === "success") { const clips = result.output.result; for (const clip of clips) { console.log(`Title: ${clip.title}`); console.log(`Audio: ${clip.audio_url}`); console.log(`Cover: ${clip.image_url}`); console.log(); } } else { console.log(`Task failed: ${result.error}`); } ``` ### Downloading the audio Audio URLs point to the generated audio file. Take the file extension from the response's `Content-Type` rather than hard-coding one — the container format is determined upstream and has changed before. You can download them directly: ```python Python theme={null} import mimetypes import shutil import urllib.request clip = clips[0] with urllib.request.urlopen(clip["audio_url"]) as response: ext = mimetypes.guess_extension(response.headers.get_content_type()) or ".audio" filename = f"{clip['title']}{ext}" with open(filename, "wb") as f: shutil.copyfileobj(response, f) print(f"Downloaded: {filename}") ``` ```javascript Node.js theme={null} import { writeFile } from "fs/promises"; const EXTENSIONS = { "audio/mpeg": ".mp3", "audio/mp4": ".m4a", "audio/wav": ".wav", }; const clip = clips[0]; const audio = await fetch(clip.audio_url); const buffer = Buffer.from(await audio.arrayBuffer()); const type = (audio.headers.get("content-type") || "").split(";")[0]; const filename = `${clip.title}${EXTENSIONS[type] || ".audio"}`; await writeFile(filename, buffer); console.log(`Downloaded: ${filename}`); ``` *** ## Full example Putting it all together: ```python Python theme={null} import requests import time API_KEY = "YOUR_API_KEY" BASE_URL = "https://sunor.cc/api/v1" # 1. Create task print("Creating music task...") response = requests.post( f"{BASE_URL}/task", headers={ "Content-Type": "application/json", "x-api-key": API_KEY, }, json={ "model": "suno", "task_type": "music", "input": { "gpt_description_prompt": "A chill lo-fi beat for studying", "make_instrumental": True, }, }, ) task_id = response.json()["data"]["task_id"] print(f"Task ID: {task_id}") # 2. Poll for results print("Waiting for completion...") while True: response = requests.get( f"{BASE_URL}/task/{task_id}", headers={"x-api-key": API_KEY}, ) data = response.json()["data"] if data["status"] in ("success", "failure", "timeout"): break time.sleep(5) # 3. Use the output if data["status"] == "success": for clip in data["output"]["result"]: print(f" {clip['title']}: {clip['audio_url']}") else: print(f"Failed: {data['error']}") ``` ```javascript Node.js theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://sunor.cc/api/v1"; // 1. Create task console.log("Creating music task..."); const createRes = await fetch(`${BASE_URL}/task`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY, }, body: JSON.stringify({ model: "suno", task_type: "music", input: { gpt_description_prompt: "A chill lo-fi beat for studying", make_instrumental: true, }, }), }); const taskId = (await createRes.json()).data.task_id; console.log(`Task ID: ${taskId}`); // 2. Poll for results console.log("Waiting for completion..."); let data; while (true) { const res = await fetch(`${BASE_URL}/task/${taskId}`, { headers: { "x-api-key": API_KEY }, }); data = (await res.json()).data; if (["success", "failure", "timeout"].includes(data.status)) break; await new Promise((r) => setTimeout(r, 5000)); } // 3. Use the output if (data.status === "success") { for (const clip of data.output.result) { console.log(` ${clip.title}: ${clip.audio_url}`); } } else { console.log(`Failed: ${data.error}`); } ``` *** ## Next steps Learn about inspiration, custom, and continuation modes. Handle errors and implement retries. Stay within rate limits with smart polling. Full endpoint documentation. # Sunor API Documentation Source: https://docs.sunor.cc/introduction Sunor API documentation for unified Suno and Udio access, including authentication, credits, billing controls, REST endpoints, and code examples. sunor is a **Sound & Music Aggregated API Platform** that gives you unified access to multiple AI music and audio generation models through a single REST API. ## What you can do * **Generate music** from text descriptions, custom lyrics, or by extending existing clips * **Generate lyrics** from a text prompt * **Upload audio** for use with AI models * **Concatenate clips** into longer compositions ## How it works No subscriptions. Top up credits and only pay for what you use. 1 credit = \$0.01 USD. Simple JSON-based API with standard HTTP methods. Submit a task, poll for results. Access different AI music providers through one unified interface. Submit tasks and retrieve results when they are ready. No long-running connections needed. ## Quick start Get up and running in four steps: Create an account at [sunor](https://sunor.cc/login) using Google OAuth. Navigate to [Dashboard > API Keys](https://sunor.cc/dashboard/api-keys) and create a new key. Go to [Dashboard > Billing](https://sunor.cc/dashboard/billing) and add credits by card through Stripe or by cryptocurrency through OxaPay. Submit a music generation task: ```bash theme={null} curl -X POST https://sunor.cc/api/v1/task \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_API_KEY" \ -d '{ "model": "suno", "task_type": "music", "input": { "gpt_description_prompt": "A cheerful acoustic guitar song about summer", "make_instrumental": false } }' ``` ## Base URL All API requests are made to: ``` https://sunor.cc/api/v1 ``` ## Next steps Learn how to authenticate your API requests. Understand the credits system and pricing. Keep an eligible account funded with an optional threshold and monthly cap. Save billing information and download PDF invoices for completed purchases. Explore the task creation endpoint. Learn about the Suno music generation model. # Suno Model Source: https://docs.sunor.cc/models/suno Generate music, lyrics, and audio with the Suno V6 API. Supports inspiration mode, custom lyrics, continuation, and audio upload. # Suno Model **Unofficial API** — sunor is not affiliated with, endorsed by, or officially connected to Suno Inc. This is a third-party integration that provides programmatic access to Suno's music generation capabilities. Suno is an AI music generation model that can create songs with vocals, instrumental tracks, and lyrics from text descriptions. ## Available task types | Task Type | Credits | Description | | --------- | ------- | ------------------------------------------------------------- | | `music` | 10 | Generate a complete music clip with audio and optional vocals | | `lyrics` | 5 | Generate song lyrics from a text prompt | | `upload` | 1 | Upload an audio file for use as input to other tasks | | `concat` | 5 | Concatenate clips into a longer composition | ## Model version sunor generates with **Suno V6**, Suno's current model. `model_version` defaults to `"v6"`, so you do not need to set it. Suno retired every earlier model on September 9, 2026, when it launched V6. Requests that still send `"v5.5"` or `"v5"` are accepted for backward compatibility and are generated with V6 — no code change is needed. | `model_version` | Generates with | Status | | --------------- | -------------- | --------------------------------------------- | | `v6` (default) | Suno V6 | Current | | `v5.5` | Suno V6 | Retired by Suno on 2026-09-09; still accepted | | `v5` | Suno V6 | Retired by Suno on 2026-09-09; still accepted | Any other value is rejected with a `400` before credits are charged. Pricing does not depend on `model_version`. sunor generates with the base V6 model only; Suno's `v6-wild` and `v6-mini` variants are not available through sunor. *** ## Music generation modes ### Inspiration mode The simplest way to generate music. Provide a natural language description and let the AI handle everything -- lyrics, melody, arrangement, and production. **When to use**: You have a general idea but want the AI to make creative decisions. ```json theme={null} { "model": "suno", "task_type": "music", "input": { "gpt_description_prompt": "An upbeat electronic dance track with a catchy synth melody", "make_instrumental": false } } ``` **Key fields**: * `gpt_description_prompt` — Describe the music you want in plain language * `make_instrumental` — Set to `true` if you want no vocals ### Custom mode Full control over lyrics and style — provide your own lyrics and/or genre/style tags. For a **style-driven instrumental**, omit `prompt` and set `make_instrumental: true`. **When to use**: You have specific lyrics, want precise genre control, or want a tag-driven instrumental. ```json theme={null} { "model": "suno", "task_type": "music", "input": { "prompt": "[Verse]\nStars are falling from the sky\nDancing shadows passing by\n\n[Chorus]\nWe are the dreamers of the night\nChasing every fading light", "tags": "indie rock, dreamy, reverb, female vocals", "negative_tags": "heavy metal, rap", "title": "Dreamers of the Night" } } ``` **Key fields**: * `prompt` — Your lyrics (structure tags like `[Verse]`, `[Chorus]`, `[Bridge]`, `[Outro]`). Omit for an instrumental. * `tags` — Comma-separated style descriptors * `negative_tags` — Styles to explicitly avoid * `make_instrumental` — Set to `true` for an instrumental with no vocals (pair with `tags`) * `title` — Song title ### Continuation mode Extend an existing clip from a specific point. Use this to build longer songs by continuing from where a previous generation ended. **When to use**: You want to extend a clip you already generated. ```json theme={null} { "model": "suno", "task_type": "music", "input": { "continue_clip_id": "abc123-clip-id", "continue_at": 30, "prompt": "[Bridge]\nBut tonight we let it go" } } ``` **Key fields**: * `continue_clip_id` — The clip ID to continue from. This can be a clip from a previous music task output **or an uploaded audio clip** (see [Audio upload](#audio-upload) — upload your audio, then pass the returned clip ID here to extend it into a full song) * `continue_at` — Timestamp in seconds (within the source clip) to start the continuation * `prompt` — Optional lyrics for the continuation *** ## Lyrics generation Generate song lyrics from a text description. ```json theme={null} { "model": "suno", "task_type": "lyrics", "input": { "prompt": "A melancholy ballad about leaving home for the first time" } } ``` *** ## Audio upload Upload an audio file by providing a publicly accessible URL. The upload returns a **clip** that you can then feed into a [continuation](#continuation-mode) to **extend your audio into a full song**. ```json theme={null} { "model": "suno", "task_type": "upload", "input": { "url": "https://example.com/my-audio.mp3" } } ``` The upload response includes a clip `id` (at `output.result[0].id`). Pass that `id` as `continue_clip_id` in a `music` task to generate a full song from your uploaded audio: ```json theme={null} { "model": "suno", "task_type": "music", "input": { "continue_clip_id": "", "continue_at": 3 } } ``` `continue_at` is the timestamp (in seconds, within your uploaded clip) to continue from. Continuation **extends** your uploaded audio and keeps its existing style. Generating a new song in a *different* style from an uploaded reference (an "audio cover") is **not currently supported**. *** ## Clip concatenation Combine clips into a single longer track. Useful for assembling a full song from individually generated sections. ```json theme={null} { "model": "suno", "task_type": "concat", "input": { "clip_id": "abc123-clip-id" } } ``` *** ## Tips for better results Instead of "a good song", try "an upbeat indie pop song with jangly guitars, handclaps, and a catchy whistle hook in the chorus". In custom mode, use `[Verse]`, `[Chorus]`, `[Bridge]`, `[Outro]`, and `[Intro]` to control song structure. Use `negative_tags` to steer away from unwanted styles rather than only specifying what you want. sunor generates with Suno V6, Suno's latest model, without any model setting in your request. # Udio Model Source: https://docs.sunor.cc/models/udio Generate music with the Udio AI music model. Single-call generation with optional user-supplied lyrics or instrumental output. # Udio Model **Unofficial API** — sunor is not affiliated with, endorsed by, or officially connected to Udio Inc. This is a third-party integration that provides programmatic access to Udio's music generation capabilities. Udio is an AI music generation model that creates songs with vocals or instrumental tracks from a short style description. Compared to Suno, Udio's strength is rich production polish and natural vocal delivery on shorter prompts. ## Available task types | Task Type | Credits | Description | | --------- | ------- | ----------------------------------------------------------------------------- | | `music` | 5 | Generate a music clip from a short prompt. Returns \~2 audio tracks per call. | Other operations (extend, remix, voice clone, audio upload) are not yet available for Udio. Use the [Suno model](/models/suno) for upload, lyrics-only, and concatenation tasks. ## Model version sunor uses Udio's **udio32-v1.5** model. Set automatically — no need to specify it. *** ## Music generation ### Quick generate (Udio writes lyrics) The simplest way to use Udio: a short style description. Udio's prompt expansion writes lyrics that match. ```json theme={null} { "model": "udio", "task_type": "music", "input": { "prompt": "lofi hip hop, chill, rainy night, jazz piano" } } ``` **Key fields**: * `prompt` — Short style/mood description. Udio's GPT layer expands it. **Migrating from Suno?** Udio uses `prompt` for the style description. The Suno-style `gpt_description_prompt` and `make_instrumental` fields are **not** accepted — submissions using them will be rejected with a `400` error. For instrumental output, set `lyrics_type: "instrumental"` (see below). ### With user-supplied lyrics Pass your own lyrics and tell Udio to use them verbatim. ```json theme={null} { "model": "udio", "task_type": "music", "input": { "prompt": "indie folk, acoustic, melancholy", "lyrics": "[Verse]\nWalking down the midnight road\nStars above begin to glow\n\n[Chorus]\nWe are the dreamers in the night", "lyrics_type": "user" } } ``` **Key fields**: * `lyrics` — Your lyrics, with section markers (`[Verse]`, `[Chorus]`, `[Bridge]`). * `lyrics_type` — Set to `"user"` to use your lyrics verbatim. ### Instrumental (no vocals) ```json theme={null} { "model": "udio", "task_type": "music", "input": { "prompt": "cinematic orchestral, sweeping strings, dramatic", "lyrics_type": "instrumental" } } ``` **Key fields**: * `lyrics_type: "instrumental"` — No vocal track in the output. *** ## Optional fields | Field | Type | Description | | ------------- | ------- | ----------------------------------------------------------- | | `tags` | string | Comma-separated style tags. Udio merges them with `prompt`. | | `seed` | integer | Deterministic seed. Omit for random. | | `lyrics_type` | enum | `generate` (default), `user`, or `instrumental`. | *** ## Tips for better results Udio's GPT layer expands a short style description into a full prompt. Long descriptions can fight that expansion. Aim for 5–15 words. When supplying your own lyrics, use `[Verse]`, `[Chorus]`, `[Bridge]`, `[Outro]` to control structure. One Udio task returns \~2 generated songs. Pick the one you like best. Both are billed as one call (5 credits). Wrong vibe? Re-submit with adjusted style words rather than tweaking the seed. Udio is sensitive to genre/mood vocabulary. *** ## Differences from Suno | | Suno | Udio | | ------------------------------------------------ | ------------------------------------------------ | ----------------------------- | | Task types | `music`, `lyrics`, `upload`, `concat` | `music` only (P1) | | Cost per call | 10 credits (\$0.10) | 5 credits (\$0.05) | | Tracks returned | 2 | \~2 | | Style description input | `gpt_description_prompt` | `prompt` | | Instrumental flag | `make_instrumental: true` | `lyrics_type: "instrumental"` | | Lyrics-only generation | Yes (`task_type: "lyrics"`) | Not yet | | Audio upload + extend | Yes (`task_type: "upload"` → `continue_clip_id`) | Not yet | | Audio cover (restyle an upload into a new style) | No | No | | Voice cloning | Not exposed via API | Not yet | # Rate Limits Source: https://docs.sunor.cc/rate-limits API rate limiting policies and response headers sunor enforces rate limits to ensure fair usage and platform stability. Limits are applied per API key across all endpoints. ## Limits | Scope | Limit | | ----------- | --------------------- | | Per API key | 120 requests / minute | All API endpoints share the same rate limit window. The limit resets every 60 seconds. ## When you hit the limit If you exceed the rate limit, the API returns a `429 Too Many Requests` response with a `Retry-After` header indicating how many seconds to wait. ```json theme={null} { "code": 429, "error_code": "rate_limited", "message": "Rate limit exceeded. Try again in 30 seconds.", "retry_after_seconds": 30 } ``` ### Response headers | Header | Sent on | Description | | ----------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `X-Request-Id` | Every public API response | Correlation identifier for logs and support | | `X-RateLimit-Remaining` | Responses after API-key authentication, plus `429` | Requests remaining in the current window; `429` returns `0`. Early `401` responses can omit it because the key was not authenticated. | | `Retry-After` | `429` only | Number of seconds to wait before retrying | ## Best practices When you receive a 429 response, wait for the duration specified in the `Retry-After` header before making another request. For automated systems, implement exponential backoff when receiving rate limit errors to avoid hammering the API. Instead of submitting many tasks simultaneously, spread submissions over time to stay within limits. Check `X-RateLimit-Remaining` in response headers to proactively slow down before hitting the limit. ## Example: rate-limit-aware polling ```python Python theme={null} import time import requests def poll_with_rate_limit(api_key, task_id, interval=5): while True: response = requests.get( f"https://sunor.cc/api/v1/task/{task_id}", headers={"x-api-key": api_key}, ) # Check rate limit headers remaining_header = response.headers.get("X-RateLimit-Remaining") if remaining_header is not None and int(remaining_header) < 5: print("Approaching rate limit, slowing down...") interval = max(interval, 10) if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 30)) print(f"Rate limited. Waiting {retry_after}s...") time.sleep(retry_after) continue data = response.json()["data"] if data["status"] in ("success", "failure", "timeout"): return data time.sleep(interval) ``` ```javascript Node.js theme={null} async function pollWithRateLimit(apiKey, taskId, interval = 5000) { while (true) { const response = await fetch( `https://sunor.cc/api/v1/task/${taskId}`, { headers: { "x-api-key": apiKey } } ); // Check rate limit headers const remainingHeader = response.headers.get("X-RateLimit-Remaining"); if (remainingHeader !== null && parseInt(remainingHeader) < 5) { console.log("Approaching rate limit, slowing down..."); interval = Math.max(interval, 10000); } if (response.status === 429) { const retryAfter = parseInt(response.headers.get("Retry-After") || "30"); console.log(`Rate limited. Waiting ${retryAfter}s...`); await new Promise((r) => setTimeout(r, retryAfter * 1000)); continue; } const { data } = await response.json(); if (["success", "failure", "timeout"].includes(data.status)) { return data; } await new Promise((r) => setTimeout(r, interval)); } } ```