The error body
// A coded refusal: the sentence, the code to branch on,
// and the page that explains the code.
{ "detail": "separation takes exactly 1 audio file (got 2)",
"code": "input_arity",
"doc_url": "https://dueta.ai/ docs/ errors-and-limits#input_arity" }
// Not every refusal is coded. Those carry the sentence alone.
{ "detail": "Not found" }
// A JSON body that failed schema validation (422): one entry per field.
{ "detail": [ {
"type": "value_error",
"loc": ["body", "email"],
"msg": "value is not a valid email address: An email address must have an @-sign."
} ] }Status codes
| Status | Meaning | Fix |
|---|---|---|
| 400 | The request reached the endpoint but could not be turned into an upload or a job: a bad filename, the wrong number of inputs, a malformed Content-Range, audio that will not decode, is under 0.5 s, or is digital silence, a total duration over the per-job cap, or Multi Mic Separation inputs that are not the same take. | Read detail; it names the field or the file. Do not retry unchanged. |
| 401 | No credential, a malformed one, a key that has been revoked or rotated away, a key sent to a console route that takes a signed-in session instead, a key of an account that was closed or disabled, or an organization key whose organization was closed or suspended. | Re-issue the key on the console's API keys page. A rotated key stops working the moment its replacement is shown. For a closed or disabled account, contact support: no request from it succeeds. Every 401 carries WWW-Authenticate: Bearer. |
| 402 | Not enough credit to cover the job, checked before it is queued. Uploads are never charged, so only job creation answers this. With an organization key, also spend_limit_reached: the organization or the key reached its monthly limit; context carries scope (org or key), limit_usd, used_usd and resets_at; limit_usd and used_usd are null for an organization's limit when the key's member may not see its billing. | Add credit in the console, then send the same request again: nothing was charged and the uploads are untouched. POST /v1/jobs/quote answers sufficient_credit before you commit. The context of insufficient_credit says where the shortfall is: organization, the name of the organization whose balance fell short, and billing_url, that balance's Billing page. organization is null for an account's own organization that has not been named. With an organization key and no billing permission, balance_usd, shortfall_usd and billing_url are null; the organization's Owners and Billing members add its credit and set its limits. A monthly limit resets at resets_at. |
| 404 | No upload, job, or stem with that name under your account; or a result that is not available yet or has been pruned. | Ids are scoped to the owning account. Take stem names from result.stems[].name and upload ids from the create response. |
| 409 | The request is out of order. Everything that stores or spends needs a confirmed email: upload create and content and job creation answer 409 email_unverified until it is. Upload content: the piece did not start at bytes_received, or the upload is already complete. Delete upload: a job still uses it. Job creation: an input is not ready, or a request under the same Idempotency-Key is still running. Cancel: the job is already terminal. | For an offset mismatch, detail names the byte to send from. For a cancel on a finished job, treat it as success. For an unverified email, click the link in the verification message. An input that is not ready carries the code input_not_ready and names the uploads that are not finished. Wait for each to complete, then resubmit the same ids. A key still in flight sends Retry-After; repeat the request with the same key after it. |
| 410 | The upload expired. An unused upload is swept with its bytes 24 hours after creation. | Create a new upload and send the file again. A job hardlinks its inputs, so expiry never takes a finished job's audio away. |
| 413 | The body exceeded the 100 MB per-file cap. A declared size_bytes over the cap is refused before a byte moves; a body that grows past it is refused mid-stream. Any other request body over 1 MiB, or a JSON job submission over 64 KiB, is refused before it is read, with no code. | Split or shorten the input. The upload is left failed; sending again from byte 0 reopens it. An oversized JSON body is a client bug: nothing the API reads is that large. |
| 415 | The file is video (.mp4, .mov, .mkv, .webm, .avi, .m4v by name, or a picture track found by the probe), or a PUT to content_url was sent as a multipart form. | Export the audio track (M4A, WAV, MP3) and upload that. To content_url, send the raw bytes with Content-Type: application/octet-stream. |
| 422 | A JSON body or query failed schema validation (a top-level field the endpoint does not read, a filter value outside its list, from after to on GET /v1/usage), ?format= on a stem download is not wav or flac, or an Idempotency-Key was reused for a different request body. | Schema validation: detail is an array of {loc, msg, type}; loc points at the offending field. A reused key: detail is a sentence with a code; branch on the code. A key names one request, so send a new key for a new one. |
| 429 | A rate limit, the ceiling on jobs in progress, the ceiling on recent unbilled jobs, or the ceiling on open event streams. | Wait Retry-After seconds, never zero. A rate limit is the one that carries retry_after_seconds and limit in the body; the two job ceilings do not, and clear when a job of yours ends rather than on a timer. Open event streams answer too_many_streams: close one, or poll. |
| 503 | A feature is closed rather than broken: FLAC transcoding is unavailable for a stem, or the job queue could not be reached. Or the service is busy: your balance was locked by another request for 5 s, or no connection was free ("The service is busy. Try again shortly.", no code). POST /v1/uploads/{id}/complete is busy the same way when no whole-file slot is free, or when your account already has 2 of its own uploads finishing on that server ("Your other uploads are still finishing"). | For FLAC, download the WAV. For the queue and busy, wait Retry-After and send the same request; nothing was charged. |
| 504 | A FLAC transcode took too long. | Retry, or download the WAV. |
| 5xx | A fault on our side. | Retry with exponential backoff. A failed job releases its hold and is never charged. |
Rate limits
| Route | Limit | Keyed on | Note |
|---|---|---|---|
| Every signed-in GET/HEAD | 600 per minute | credential | All routes together. Per API key, or per signed-in account. RateLimit-* on the 429 only. |
| POST /v1/uploads | 20 per minute | account | Its own budget. Submitting a job does not draw on it. |
| PUT /v1/uploads/{id}/content | 20 per minute | account | Its own budget, every Content-Range piece counted. The same for POST /v1/uploads/{id}/complete. |
| POST /v1/jobs/music-separation | 20 per minute | account | Its own budget. With source_url (a YouTube link) the same budget, plus the row below. |
| POST /v1/jobs/music-separation/probe | 20 per minute | account | Its own budget. A probe is $0; its answer is good for 10 minutes and the same link is one probe. |
| Music Source Separation jobs fetched from a link | 2 at once | account | Not a rate limit: queued or running jobs made with source_url. 429 with Retry-After until one finishes. Published by GET /v1/limits as products[music-separation].source_url.max_in_flight. |
| Links being read (probes) | 2 at once | account | Not a rate limit: probes queued or running. 429 rate_limited with Retry-After. While 8 probes wait, a new one is 503 queue_full with Retry-After; a link already answered is never refused. |
| POST /v1/jobs/separation | 20 per minute | account | |
| POST /v1/jobs/multi-mic-separation | 20 per minute | account | Counted apart from separation, not with it. |
| POST /v1/jobs/target-speaker-extraction | 20 per minute | account | Its own budget. |
| POST /v1/jobs/noise-remover | 20 per minute | account | Its own budget. |
| POST /v1/jobs/quote | 600 per minute | account | Its own budget. |
| DELETE /v1/jobs/{id} | 30 per minute | account | One budget with POST /v1/jobs/{id}/restore. |
| GET /v1/limits | 60 per minute | IP | Cached 60 s; read it once, not per request. |
| Submitting a job by url | 30 per hour | IP | Only the fetch-on-your-behalf path. Sending the file instead is unaffected. |
| GET /v1/jobs/{id}/events | 30 per minute | account | The same for /v1/uploads/{id}/events. A browser session and an API key are counted apart. |
| Jobs in progress | 1 at once, 4 after a purchase | account | Not a rate limit: queued or running. 429 too_many_active_jobs with Retry-After until one finishes. |
| Open event streams | 16 at once | credential | Not a rate limit: concurrent SSE subscriptions. 429 too_many_streams with Retry-After. A browser session and an API key are counted apart. |
| Unbilled jobs | 20 per hour | account | Not a rate limit: jobs that failed or were canceled without a charge. |
Separate budgets. GET /v1/limits returns them, no credential.
| Header | Meaning |
|---|---|
| RateLimit-Limit | The ceiling for the window. |
| RateLimit-Remaining | Requests left in it. 0 on the refusal itself. |
| RateLimit-Reset | Whole seconds until the window rolls. |
| X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset | The same three, older names. |
| Retry-After | Whole seconds to wait. On a 429 only. |
HTTP/ 1.1 429 Too Many Requests
Retry-After: 60
RateLimit-Limit: 20
RateLimit-Remaining: 0
RateLimit-Reset: 60
{ "detail":
"Too many requests. The limit here is 20 per minute; wait 60 seconds and try again.",
"code": "rate_limited",
"doc_url": "https://dueta.ai/ docs/ errors-and-limits#rate_limited",
"retry_after_seconds": 60,
"limit": "20 per minute" }Five budgets answer 429. Branch on code.
| code | Meaning |
|---|---|
| rate_limited | The request rate. Wait retry_after_seconds and repeat; RateLimit-* headers say where you stand. |
| too_many_active_jobs | Your jobs, not your requests: one at a time before the first purchase, four after. Retry when one of yours ends, not on a timer. No RateLimit-* headers. |
| too_many_in_flight | Your music link checks, not your requests. Retry when one ends, not on a timer. No RateLimit-* headers. |
| too_many_unbilled | Too many recent jobs finished without a charge. Clears as the window rolls. |
| too_many_streams | Open event streams on one credential. Close one, or poll the resource. |
| url_fetch_quota | The hourly budget for submitting a job by url. Send the file instead. |
Honor Retry-After.
r = session.post(url, json=body, headers=headers)
if r.status_code == 429:
code = r.json().get("code")
if code == "rate_limited":
time.sleep(int(r.headers["Retry-After"])) # a window: waiting works
r = session.post(url, json=body, headers=headers)
elif code == "too_many_streams":
... # an event stream: close one, or poll
else:
... # a job slot: wait for one to end
# Back off before the wall: a limited POST carries what is left (a read, on its 429 only).
if int(r.headers.get("RateLimit-Remaining", 1)) == 0:
time.sleep(int(r.headers["RateLimit-Reset"]))Codes
Coded refusals carry code and doc_url.
| code | Status | Meaning |
|---|---|---|
| rate_limited | 429 | The request-rate limiter refused. Wait retry_after_seconds and repeat. |
| too_many_in_flight | 429 | This account already has as many music link checks or link-fetched jobs running as it may. Retry when one of them ends, not on a timer. |
| too_many_active_jobs | 429 | This account already has context.limit jobs queued or running: one before its first purchase, four after (context.tier). Retry when one ends, or cancel one. |
| too_many_unbilled | 429 | Too many recent jobs ended without a charge. Clears as the window rolls. |
| too_many_streams | 429 | Close one of this credential's open event streams, or poll the resource instead. context.max_open_streams is the cap per server process. |
| url_fetch_quota | 429 | Send the file instead: the per-address budget for fetching a submitted URL is spent. |
| queue_full | 503 | The shared queue is full. Retry after retry_after_seconds; the context carries the depth and the estimated wait. |
| gpu_unavailable | 503 | The worker is alive and has declined to take work: its GPU breaker is open. Retry after retry_after_seconds. |
| input_arity | 400 | The product does not take this many files. |
| input_not_ready | 409 | One or more named uploads has not finished transferring. |
| upload_failed | 409 | One or more named uploads failed (its error says why: unreadable, silent, too long). Sending the same bytes again fails the same way: fix the file and create a new upload. GET /v1/uploads/{id} error_code stays upload_failed; its reason_code is the specific input_* refusal (input_unreadable, input_too_short, input_silent, input_too_long, input_audio_format, input_is_video, input_file_too_large), or null. |
| input_total_too_long | 400 | Every input added together is over the per-job ceiling. |
| input_too_long | 400 | One recording is longer than the step processes in a single pass. |
| input_range_invalid | 422 | range cannot be used as sent: the recording is 30 s or shorter (a range is offered only on longer ones), start_seconds is not before end_seconds, the range does not lie within the recording (for a source_url, the length its probe read), or it is under 30 s. context carries start_seconds, end_seconds, duration_seconds and min_seconds. |
| submission_collided | 409 | The same submission was sent twice at once and the other one finished before this one could be matched to it. Nothing was charged. Send it again after Retry-After; context carries retry_after_seconds. |
| input_range_busy | 503 | range could not be cut right now because the service is busy. Nothing was charged and no job was made. Send the same request again after Retry-After; context carries retry_after_seconds. |
| input_not_one_take | 400 | The set is not one take: the longest is too many times the shortest. |
| input_unequal_length | 400 | The model needs every input to be the same length, and they are not. |
| input_too_many_files | 400 | More input files than one job may carry. |
| input_file_too_large | 413 | One file is over the per-file byte ceiling. |
| upload_quota | 429 | The account holds as many unused uploads as it may (20 still being sent, 100 in all, or 5 GB), or sent 50 GB in the last 24 hours. Use, finish or delete an upload, or wait. Unused uploads expire after a day. |
| input_unreadable | 400 | The file is not audio this service can read: empty, damaged, non-finite samples, or not an audio format at all. Export it again as audio. |
| input_too_short | 400, failed | The recording is shorter than the 0.5 s minimum. Target Speaker Extraction also fails a recording under 2 s, with details seconds and min_seconds; the job was not charged. |
| input_silent | 400 | The recording holds only digital silence. |
| input_audio_format | 400 | The audio has more than 8 channels or a sample rate above 192 kHz. |
| input_is_video | 415 | The file carries a video track. Export the audio track and send that. |
| input_file_type | 400 | The file's extension is not an audio format this service reads; context.allowed lists the ones it does. |
| input_filename | 400 | The filename is empty, longer than 200 bytes (66 Korean characters), or contains control characters. When it is too long, context carries max_bytes and max_korean_chars. |
| insufficient_credit | 402 | Add credit: the quote is more than the balance holds. context carries balance_usd, required_usd and the shortfall_usd to cover; balance_usd and shortfall_usd are null for an organization key without the billing permission. |
| spend_limit_reached | 402 | This job would take an organization, or one of its API keys, past its monthly limit. context carries the job's required_usd and, when you may see them, limit_usd and used_usd. |
| email_unverified | 409 | The address has never been confirmed. Confirm it from the link in the signup mail, or ask for a new one from Settings; nothing else unblocks the request. |
| download_limit_reached | 403 | This job's archive cannot be downloaded again. Single stems, previews and the original uploads are unaffected. |
| purchase_required | 403 | Returned only when a purchase gate is enabled. Downloads currently do not require a credit purchase. |
| phone_invalid | 422 | The number is not one an SMS can reach: write it in E.164 (+821012345678) or, for a Korean mobile, as the country writes it (010-1234-5678). |
| sms_unavailable | 503 | No code could be sent or checked: SMS is not configured on this deployment, or the provider did not answer. When the provider could not be reached nothing was consumed; when it did not answer in time the send or try counts, since the SMS may have gone out. Retry-After and context.retry_after_seconds say when to try again. |
| phone_send_cooldown | 429 | A code was sent too recently, or the day's sends for this account or number are spent; context.retry_after_seconds says when the next may go. |
| phone_code_invalid | 400 | The code did not match, expired, or was voided after too many wrong tries. Ask for a new one; the number is unchanged. |
| phone_in_use | 409 | The number is already verified on another account. One account per number. |
| invalid_api_key | 401 | The key is not a live key: mistyped, revoked, rotated away, or its organization was closed. Copy it again, or create a new one in the console. |
| api_key_placement | 401 | Send the API key as Authorization: Bearer <key>, not in another header. |
| session_only | 401 | This route takes a signed-in session, not an API key. A key opens the routes on these docs. |
| coupon_invalid · Console only | 404 | The code does not exist, is switched off, or is not eight to sixteen letters and digits. Check the spelling. |
| coupon_expired · Console only | 410 | The code's expiry date has passed. Nothing was applied. |
| coupon_not_started · Console only | 422 | The code is not active yet; context.starts_at says when it opens. |
| coupon_exhausted · Console only | 410 | Every use of this code has been taken. Nothing was applied. |
| coupon_used · Console only | 409 | This account has already used the code as many times as it allows. Nothing was applied. |
| coupon_kind · Console only | 422 | A credit code was sent to the checkout, or a discount code to redeem: use the other route. |
| callback_already_registered | 409 | The job already has a different callback_url, and a callback cannot be moved once registered. Read the job to see where its deliveries go; context carries job_id and the callback_url that stands. The same URL twice is an ordinary retry and is not this. |
| idempotency_in_flight | 409 | Wait and retry: the first request under this key has not answered yet, and the retry gets its answer. Retry-After says how long. |
| idempotency_body_mismatch | 422 | Use a new key. This one already names a different request, and no amount of waiting changes that. |
| upload_in_progress | 409 | Wait and resume: another transfer holds this upload's writer lease and is still sending. |
| upload_lease_lost | 409 | Create a new upload and send the file again: this transfer's lease went to another one, so nothing it sent was kept. |
| upload_already_ready | 409 | Nothing to send: this upload already has every byte and is certified. Create a new upload for a different recording. |
| upload_offset_mismatch | 409 | Resume from context.bytes_received, which is where the file actually ends; context.sent_from is where this piece started. |
| upload_not_found | 404 | No upload by that id. Check the ids in context.upload_ids, then create one with POST /v1/uploads and send its bytes before submitting. |
| body_shape | 400 | The body is not a submission. Send inputs naming uploads, or url, or the file as multipart: one of the three. |
| currency_unsupported | 422 | Leave currency out, or send one of context.supported: no payment account here charges context.currency. |
| enrollment_is_mixture | 422 | Send a separate sample of the target speaker as inputs[1]: it is the same audio as the recording in inputs[0]. |
| enrollment_too_short | 422, failed | Send a longer sample of the target speaker: 3 s of speech at least, 10 s or more recommended. A refusal's context carries min_seconds and given_seconds; a failed job was not charged. |
| enrollment_too_long | 422 | Send a sample of the target speaker of 300 s or less, after the recording. context carries max_seconds and given_seconds. |
| enrollment_no_speech | failed | Send a sample in which the target speaker talks. The job failed and was not charged. |
| silent_output | failed | Output too quiet. The job failed and was not charged. |
| target_not_found | failed | Check the sample is the voice of someone in the recording. The job failed and was not charged. |
| input_too_many_channels | 422 | Send a mono or stereo file: Music Source Separation takes nothing wider. context carries channels and max_channels. |
| music_too_many_channels | failed | Send a mono or stereo file: this one has more than two channels (context.channels). The job failed and was not charged. |
| probe_invalid | 422 | probe_id cannot price this link: not your probe or older than 10 minutes (context.state unknown), not answered yet (pending), or a probe of another video (mismatch). Probe the link again. |
| source_url_invalid | 422 | Send a YouTube video link (youtube.com/watch?v=..., youtu.be/..., a short): this is not one, or it names a playlist, a channel or another site. context carries url. |
| source_unavailable | failed | The video cannot be fetched: private, removed, restricted in this region or by age, or YouTube refused the request. The job failed and was not charged. |
| source_blocked | failed | The platform refused our server's request (a bot check, a 403 or a 429): a fault on our side, not the video's state (that is source_unavailable). Try again later. The job failed and was not charged. |
| source_too_long | failed | The video is longer than the length limit (GET /v1/limits, music-separation max_input_seconds); context carries seconds and max_seconds. The job failed and was not charged. |
| source_live | failed | The link is a live or upcoming stream; only a finished video can be separated. The job failed and was not charged. |
| source_fetch_failed | failed | Fetching the video did not finish (a timeout, the size limit, a decode failure). Retry the job; it was not charged. |
| source_insufficient_credit | failed | The song is longer than the credit held for it could pay for: the hold was the smaller of the length cap's cost and your balance. Top up and submit the link again. details carries seconds, cost_usd and held_usd. The job failed and was not charged. |
| music_too_long | failed | Send a song within the length limit (GET /v1/limits, music-separation max_input_seconds); context carries seconds and max_seconds. The job failed and was not charged. |
| input_malware_detected | failed | Upload a clean copy of the file: the malware scan flagged this input and it was not processed. The job failed and was not charged. |
| input_quiet | 400 | The whole recording sits far below speech level. |
| input_mostly_silent | 400 | Almost none of the recording is above the silence floor. |
| input_narrowband | 400 | The source was captured below the rate the model works at. |
| preview_unavailable | 404 | Play and draw from the WAV stems instead: this job has no preview or peaks (it finished before they existed, or they could not be made). result.previews says so before you ask. |
| not_allowed | 403 | The caller is authenticated but may not do this, and the route named no more specific code. Check the key or session belongs to the account or organization that owns the resource. |
| not_found | 404 | No such resource or path, or one this caller may not see, and the route named no more specific code. Check the id and the path against the endpoint index. |
| method_not_allowed | 405 | The path exists and does not take this HTTP method. Check the method against the endpoint index. |
A failed job carries error_code from this list, or null.
| error_code | Meaning |
|---|---|
| enrollment_no_speech | Send a sample in which the target speaker talks. The job failed and was not charged. |
| enrollment_too_short | Send a longer sample of the target speaker: 3 s of speech at least, 10 s or more recommended. A refusal's context carries min_seconds and given_seconds; a failed job was not charged. |
| target_not_found | Check the sample is the voice of someone in the recording. The job failed and was not charged. |
| input_too_short | The recording is shorter than the 0.5 s minimum. Target Speaker Extraction also fails a recording under 2 s, with details seconds and min_seconds; the job was not charged. |
| music_too_many_channels | Send a mono or stereo file: this one has more than two channels (context.channels). The job failed and was not charged. |
| music_too_long | Send a song within the length limit (GET /v1/limits, music-separation max_input_seconds); context carries seconds and max_seconds. The job failed and was not charged. |
| source_unavailable | The video cannot be fetched: private, removed, restricted in this region or by age, or YouTube refused the request. The job failed and was not charged. |
| source_too_long | The video is longer than the length limit (GET /v1/limits, music-separation max_input_seconds); context carries seconds and max_seconds. The job failed and was not charged. |
| source_live | The link is a live or upcoming stream; only a finished video can be separated. The job failed and was not charged. |
| source_fetch_failed | Fetching the video did not finish (a timeout, the size limit, a decode failure). Retry the job; it was not charged. |
| source_insufficient_credit | The song is longer than the credit held for it could pay for: the hold was the smaller of the length cap's cost and your balance. Top up and submit the link again. details carries seconds, cost_usd and held_usd. The job failed and was not charged. |
| silent_output | Output too quiet. The job failed and was not charged. |
| source_blocked | The platform refused our server's request (a bot check, a 403 or a 429): a fault on our side, not the video's state (that is source_unavailable). Try again later. The job failed and was not charged. |
| input_malware_detected | Upload a clean copy of the file: the malware scan flagged this input and it was not processed. The job failed and was not charged. |
Limits & retention
| Limit | Value |
|---|---|
| Formats | WAV, AIFF, FLAC, MP3, M4A / AAC, OGG / Opus, WMA, CAF, or AMR. Video refused. |
| File size | 100 MB per file. |
| 30 min under 100 MB | MP3 or M4A. |
| Duration per file | At least 0.5 s, not digital silence. |
| Duplex Conversation Separation recording | 30 min. |
| Duration per job | 120 min, all inputs summed. |
| Inputs per job | Duplex Conversation Separation 1. Multi Mic Separation 2–10, longest at most 2× the shortest. |
| Unused upload | Swept after 24 hours. |
| Results and inputs | Kept 7 days after the job finishes; after a purchase (demo and full refunds excluded), until you delete them. An organization key's jobs follow the organization's purchases. |
| API keys | Do not expire. |