Errors
When a request does not work, the response has an HTTP status code of 400 or more and a JSON body in the standard form of
RFC 9457 (application/problem+json). Your program should look at the code - it never changes - not at
the wording of the sentence:
{
"type": "https://www.recuromedia.com/menu.developer.php#BalanceInsufficient",
"title": "Balance too low",
"status": 402,
"code": "BalanceInsufficient",
"detail": "Your balance is too low for this job. Please top up to start it.",
"retryable": false,
"request_id": "req_6c1f0b9e2d4a",
"details": { "account_plan": "Prepaid", "required_eur": "12.40", "balance_eur": "3.10" }
}| Field | What it is |
|---|---|
code | The error code: one word that says what happened (the table below). Test this in your program. |
status | The HTTP status code again. |
title | A short title of the code. |
detail | A sentence about this case that you can show to a person. |
retryable | true: the same request, unchanged, can work later - after Retry-After when the response has that header, or once the step the code's row names is done (for example a new token after TokenExpired, an open upload completed or deleted after TooManyUploads). false: the request itself must change. |
request_id | The request's id (also the Recuro-Request-Id header). Quote it when you contact us. |
details | Facts of this case, where useful - for example {"missing": [4]} for UploadIncomplete, {"job_id": "job_3150"} for JobExistsForMedia, {"field": "settings"} for ValidationFailed. Missing when there are none. |
type | A link to the code's row in the table below. |
A simple way to handle errors: on 429 and 503 with a Retry-After header wait
that many seconds and send the same request again - except PreviewLimitReached (it can
say up to a day: start the job without a preview, or wait) and TooManyUploads (complete or
delete an open upload first); a 503 without Retry-After (FeatureUnavailable,
not_enabled or not_configured) means the feature is not offered - do not retry. On 401
TokenExpired get a new token and send it again; on 500 and 502 try again after a few seconds, at
most 3 times; on any other error stop and show detail - repeating it unchanged will not help.
HTTP status codes
| Status | What it means for you | What to do |
|---|---|---|
| 200 OK | The request worked; the body holds the answer. | Read the body. |
| 201 Created | Something new was made (an upload, a job, a sign-up, a link). The Location header, where present, is its address. | Keep the id of the body. |
| 202 Accepted | We took the request and work on it now (an analysis, a preview, a job that runs on). The body shows the state at this moment. | Poll the thing until it has finished. |
| 204 No Content | The request worked and there is nothing to send back (a delete, a sign-out). | Nothing. |
| 206 Partial Content | The part of a file you asked for with Range. | Add it to the part you have. |
| 304 Not Modified | Nothing has changed since the ETag you sent in If-None-Match; there is no body. | Keep using what you have; poll again after Retry-After. |
| 400 Bad Request | Your request is wrong: a missing or wrong field, broken JSON. | Correct it as code and detail say; repeating it unchanged will not help. |
| 401 Unauthorized | We do not know who you are: no key, a wrong key or secret, an expired or ended token. | Check the credentials; get a new token. |
| 402 Payment Required | The money or seconds are not there, or an order is not paid. | Top up or pay on the website. |
| 403 Forbidden | We know who you are, but you are not allowed to do this. | See the error code; often it needs an API key, another plan or API access. |
| 404 Not Found | The address or the id does not exist (or is not yours). | Check the address and the id. |
| 405 Method Not Allowed | The address exists with other HTTP methods. | Use a method of the Allow header. |
| 409 Conflict | The thing is not in the right state for this request yet or any more (still being analysed, already started, not waiting for keyframes). | See the error code: wait, or change what you do. |
| 410 Gone | It existed but has expired or been removed. | Start again (a new upload, a new sign-in). |
| 413 Content Too Large | A file, chunk or body is too large. | Send less. |
| 415 Unsupported Media Type | A file type or a body format we do not take. | Use a supported format. |
| 416 Range Not Satisfiable | A chunk number or a byte range outside the file. | Use a number or range inside it. |
| 422 Unprocessable Content | The request is well-formed but the values do not go together (settings, frames, keys). | Change the values as detail says. |
| 426 Upgrade Required | One of our apps is too old. | Update the app. |
| 429 Too Many Requests | Too many requests, or a daily or parallel limit is reached. | Wait the Retry-After seconds, then try again. |
| 500 Internal Server Error | A fault on our side. | Try again after a few seconds; contact us with the request_id if it repeats. |
| 502 Bad Gateway | A cloud server of ours does not answer. | Try again after a minute. |
| 503 Service Unavailable | A feature or server is not available at the moment, or a server is starting. | Wait the Retry-After seconds; without one, the feature is not offered. |
| 507 Insufficient Storage | Your storage (or the server's disk) is full. | Delete media and jobs you no longer need. |
Error codes
Every code the API answers with, its HTTP status, whether a retry can help (Retry: the error's retryable, explained above), what it means for you and what to do.
| Code | HTTP | Retry | What it means for you | What to do |
|---|---|---|---|---|
ValidationFailed | 400 | no | A field of your request is missing, has the wrong type or a value that is not allowed - also source or client_data larger than 4 KB (4,096 bytes) as JSON. details.field names the field. | Correct the field named in details.field as detail describes; do not send the same request again unchanged. source and client_data hold at most 4 KB of JSON each. |
InvalidJson | 400 | no | The body is not a JSON object (a syntax error, a list instead of an object, or a byte order mark before the JSON). | Check the JSON (for example with an online JSON checker). On Windows PowerShell write the file with -Encoding ascii (see Windows). |
ImportUrlFailed | 400 | no | We could not download the import address: it answered with another HTTP status than 200 (details.http_status), it could not be reached, or it redirected more than 5 times. | Open the address in a browser that is not signed in anywhere: it must download the file directly. Use a direct download link. |
ChunkSizeMismatch | 400 | no | A chunk does not have the right number of bytes: details.expected says how many it needs (every chunk is chunk_size bytes, the last one the rest of the file). | Cut the chunk again at index × chunk_size with chunk_size bytes and send it again. |
ChunkDigestMismatch | 400 | yes | A chunk does not match its Repr-Digest: it was damaged on the way. | Send the same chunk again. |
Unauthorized | 401 | no | The request has no Authorization header, a malformed one, or an access token we do not know (also one replaced by a refresh). | Send Authorization: Basic with your key and secret (curl -u KEY:SECRET) or Authorization: Bearer with a valid access token. On Windows PowerShell use curl.exe, not curl. |
InvalidCredentials | 401 | no | The API key or the secret is wrong (the response never says which), or the e-mail address or password, a refresh token or a PKCE verifier is wrong or expired. | Copy the key and the secret again (no spaces, the secret has 32 characters). Lost the secret? Create a new one (Administrator Tools). After 20 failures in 15 minutes your address is blocked for a while (RateLimited). |
TokenExpired | 401 | yes | The access token is older than an hour. | Get a new token: refresh it (apps) or exchange the key again (API keys), then repeat the request. |
TokenRevoked | 401 | no | The sign-in was ended: signed out, signed out under Connected Apps, a password change or reset, two-factor authentication switched on or off, or the account was deactivated. | Sign in again. |
RefreshTokenReused | 401 | no | A refresh token was used again more than 60 seconds after it had been replaced. For safety the app is signed out: someone else must have a copy of the token. | Sign in again, and store each new refresh token at once (an old one works only 60 seconds longer). |
InvalidCode | 401 | no | An e-mailed or authenticator code is wrong or expired. details.expired true means the code cannot be used any more; an e-mailed sign-in code also tells details.tries_left. | Ask the person to type the code again; with details.expired true ask for a new code. |
BalanceInsufficient | 402 | no | Your prepaid balance does not cover the job - or, for a keyframe change (Modify), the EUR 0.05 each of its tries needs before it starts (details.feature keyframe_modify). details.required_eur is what the job or change needs, details.balance_eur what you have. | Top up your balance on the website (links.top_up of GET /me), then start the job again. |
SecondsLeftInsufficient | 402 | no | The seconds left of your Trial do not cover the job (details.seconds_left). | Upload a shorter part. On a Trial, upgrade to Prepaid in the workspace (links.upgrade of GET /me) and top up. |
ApiAccessDisabled | 403 | no | API access is not switched on for this user. | Contact us to have API access switched on for this user. |
AccountDeactivated | 403 | no | The account is deactivated. | The company administrator or we have to activate the account again; contact your administrator or us. |
AccountNotValidated | 403 | no | The account's e-mail address is not confirmed yet. We have e-mailed a new code; details.signup is the id to send it with. | Send the e-mailed code with details.signup to POST /auth/signup/verify. |
InsufficientScope | 403 | no | This kind of sign-in is not allowed to do this. details.required names the scope it lacks (for example webhooks); details.credential api_key means only an API key can do it (URL imports). | Use an API key for webhooks and URL imports; an app uploads the file instead. |
TrialLimitExceeded | 403 | no | The media is longer than the Trial allows for this kind of job (details.max_seconds). | Upload a shorter part of the film, or upgrade to Prepaid in the workspace (links.upgrade of GET /me), then top up. |
AppLoginDenied | 403 | no | The person pressed Deny in the browser sign-in. | Stop: the person does not want this app signed in. Offer to start again. |
SignupClosed | 403 | no | This site does not take new sign-ups at the moment. | Sign up on the website later, or contact us. |
NotFound | 404 | no | No such endpoint, or no such id - also an id that belongs to another user, and anything you have deleted. | Check the address and the id. Ids are case-sensitive and include their prefix (med_, job_ ...). |
MethodNotAllowed | 405 | no | The address exists, but not with this HTTP method. The Allow header lists the methods it takes. | Use one of the methods in the Allow header (see the reference for the endpoint). |
MediaNotReady | 409 | yes | The media is still being analysed (details.status queued or analyzing), or its analysis failed (failed). | Wait: poll GET /media/{media_id} until status is ready. A failed media will not become ready: delete it and upload again. |
MediaBusy | 409 | yes | The same upload is being completed by another request right now. | Wait the Retry-After seconds (5) and complete again: you get the same media. |
JobExistsForMedia | 409 | no | The media has a job already (details.job_id). A media has one job at a time. | Use the existing job (details.job_id), or delete it first if you want other settings. |
JobNotCancellable | 409 | no | The job has ended already (details.status). | Nothing to do: the job has ended. Delete it if you do not need it. |
JobNotAwaitingKeyframes | 409 | no | The job is not waiting for its keyframes: it is not a colorization job, it has not reached the keyframes yet, or they were approved already. | Check the job's status: keyframes can be read and changed only while it is needs_action. |
ModifyInProgress | 409 | yes | A keyframe of the job is being changed (details.frame). One change per job runs at a time, and approving waits for it. | Wait the Retry-After seconds (1) and poll the keyframe of details.frame until its modify.state is done or failed; then try again. |
ModifyLimitReached | 409 | no | No longer answered: since 24 Oct 2026 the one limit of Modify is the tries a keyframe keeps at a time (KeyframeTriesFull). Kept in this list for programs that still handle it (it said the plan's tries were used up: details.frame, details.scene, details.limit, details.remaining, details.per). | Not answered any more. From an older server: choose one of the keyframe's versions instead (PUT /jobs/{job_id}/keyframes, version) - that stays free. |
PreviewsBusy | 409 | yes | Two of your previews are being made already (details.active). | Wait until one of them is ready or failed (the Retry-After says 20 seconds), or delete one. |
PreviewNotReady | 409 | yes | The preview's images are not made yet (details.status). | Poll GET /previews/{preview_id} until it is ready, then fetch the image. |
UploadIncomplete | 409 | no | Chunks are missing: details.missing lists them (at most the first 1,000). | Send the chunks in details.missing, then complete again. |
UploadNotOpen | 409 | no | The upload has been completed; it takes no more chunks. | Nothing to send: the file is a media already - media_id of GET /uploads/{upload_id} within a day of the completion; later find it with GET /media. |
ResultNotReady | 409 | yes | The job has no result yet (details.status: queued, running or needs_action). | Poll GET /jobs/{job_id} until status is completed, then download. |
NoResult | 409 | no | The job failed or was cancelled (details.status): it has no result and will never have one. | Read the job's error.message. Delete the job, then start a new one if you want to try again. |
AppLoginExpired | 410 | no | The browser sign-in has expired (after 15 minutes) or was cancelled. | Start a new browser sign-in. |
AppLoginAlreadyRedeemed | 410 | no | The browser sign-in's tokens have been collected already (they are handed out once). | Use the tokens of the first successful poll; to get new ones, sign in again. |
UploadExpired | 410 | no | No chunk arrived for 7 days: the upload was removed. | Open a new upload and send the whole file again. |
ResultExpired | 410 | no | The result has been deleted: results are kept 60 days after the job finished. | Start a new job of the media (delete the old job first), if the media still exists. |
ApiActionRetired | 410 | no | A request in the form of the old API (one address with an action field, before 6 Oct 2026). | Change your program to this API (the reference below). |
ChunkTooLarge | 413 | no | A chunk is larger than 32 MiB. | Use a chunk_size of at most 33,554,432 bytes. |
FileTooLarge | 413 | no | The file is larger than 50 GiB. | Upload the film in parts of at most 50 GiB. |
PayloadTooLarge | 413 | no | A JSON body is larger than 1 MiB (1,048,576 bytes). | Send a smaller body. A file goes in chunks (PUT /uploads/{upload_id}/chunks/{index}), never in a JSON body. |
MediaTypeUnsupported | 415 | no | The file name does not end in .mov, .mp4, .mxf, .mkv, .mpg, .mpeg, .wav or .mp3. | Convert the file into one of these formats (for example MOV) and upload that. |
ContentTypeUnsupported | 415 | no | A body that is not JSON (its Content-Type names another format) where JSON is expected. | Send the body as JSON with Content-Type: application/json. |
ChunkOutOfRange | 416 | no | The chunk number is not one of the upload's: they run from 0 to details.chunk_count - 1. | Use chunk numbers from 0 to chunk_count - 1 of the upload. |
RangeNotSatisfiable | 416 | no | The Range of a download lies outside the file (details.size). | Ask for a range inside the file (details.size bytes), or download without Range. |
OptionConflict | 422 | no | Settings that cannot be combined, or that cannot be made for this media; detail says which, details.conflicts lists them where there are several. | Change the settings as detail says (for example another output format) and send the request again. |
RangeInvalid | 422 | no | A frame or time outside the media. | Use a frame from 0 to frames - 1, or a time from 0 to duration_seconds, of the media. |
RangeNotSupported | 422 | no | A range (a part of a media) was sent: parts are not supported yet. | Leave range out and upload only the part you want processed. |
SceneNeedsKeyframe | 422 | no | Your choices would leave a scene without any keyframe (details.scene, counted from 0). | Keep at least one keyframe of that scene (excluded false). |
IdempotencyKeyReused | 422 | no | The Idempotency-Key was used within the last 24 hours for a request with another body. | Use a new key for a new request; use the same key only to repeat exactly the same request. In the shell examples: unset and the key variable of the example (UPLOAD_KEY and JOB_KEY of the workflow - its step 0 does it; KEY_UPLOADS, KEY_IMPORTS, KEY_PREVIEWS, KEY_JOBS or KEY_MODIFY of the reference) makes the next run send a new one. |
FrameRateChangedAtIngest | 422 | no | The media's 3:2 pulldown was removed at the analysis (29.97 fps became 23.976 fps), so output.frame_rate source cannot give 29.97 fps back. | Upload the file again with "ingest": {"keep_frame_rate": true}, or use output.frame_rate processed. |
PictureTooLarge | 422 | no | The picture is larger than we take (never on the Free plan): details.width and details.height (the picture as found or as you declared it), details.max_long_side and details.max_short_side (the limit, either way round). 4K is only made by upscaling (output scaling 2160). | Export the film at 2K or HD (at most max_long_side × max_short_side) and upload that. For a 4K result, choose output scaling 2160. |
PictureTooLargeForFilter | 422 | no | Reconstruct cannot work on a picture this large on our graphics cards: details.filter is reconstruct (the picture itself) or super_resolution (Reconstruct with output scaling, which doubles a picture up to 1024 × 576), details.width and details.height are the picture the filter would make, details.max_width and details.max_height the largest picture of the same shape our cards take for it and details.max_mpixels its megapixels (all three null when we cannot tell). Checked by POST /quotes and POST /jobs before anything is charged, so a job never fails for it halfway. Not on the Free plan: its job starts, and a task too large for every card fails in our processing. | Choose settings without reconstruct (for super_resolution: no output scaling), or export the film smaller - at most max_width × max_height - and upload that. |
SignupInvalid | 422 | no | The sign-up form has errors: details.errors lists each with its field, a code (FreeEmailAddress - production only, InvalidEmail, WeakPassword, Required, InvalidText - a name or company with an invisible character such as a tab or a zero-width space, or longer than its limit; any other character, åäö, emoji and every punctuation mark included, is fine, CountryNotServed - a country under sanctions, TermsNotAccepted) and a sentence (detail). | Show each entry's detail next to its field and let the person correct it. |
WeakPassword | 422 | no | The new password is too weak: it needs at least 10 characters. | Choose a password of at least 10 characters. |
ClientVersionUnsupported | 426 | no | One of our apps is older than the lowest version still accepted (details.minimum_version). | Update the app to details.minimum_version or later (the Downloads page). |
RateLimited | 429 | yes | Too many requests in a minute (details.limit per details.window_seconds), too many failed sign-ins or e-mailed codes, or a request with an Idempotency-Key waited more than 30 seconds for an earlier one of yours. | Wait the number of seconds in the Retry-After header, then send the request again. Slow down your requests (see Limits). |
SlowDown | 429 | yes | A browser sign-in was polled faster than its interval. | Poll every details.interval seconds from now on. |
TooManyParallelChunks | 429 | yes | More than 4 chunks of one upload, 16 of one credential or 32 of one user are on their way at the same time. | Wait 1 second (Retry-After) and send the chunk again; send fewer chunks at the same time. |
TooManyUploads | 429 | yes | You have 10 uploads open. | Complete or delete one of your open uploads (Retry-After says 60 seconds). |
PreviewLimitReached | 429 | yes | Today's free previews of this kind are used up (details.resets_at says when the next one is free). | Wait until details.resets_at (the Retry-After header gives the seconds), or start the job without a preview. |
InternalError | 500 | yes | Something went wrong on our side. | Try again after a few seconds. If it happens again, contact us with the request_id. |
CloudServerUnreachable | 502 | yes | The cloud server that holds this media or job does not answer. | Try again after a minute. If it lasts, contact us with the request_id. |
ExchangeRateUnavailable | 503 | yes | Your prices are in another currency than euros and today's exchange rate is not there yet. | Try again after the Retry-After seconds (3600). |
CloudServerStarting | 503 | yes | The cloud server that holds this media or job is being started. | Wait the Retry-After seconds and send the request again. |
FeatureUnavailable | 503 | yes | The feature is switched off on this site or not available at the moment (GET /health features). A keyframe change says why in details.reason: not_enabled (this site does not offer it; no Retry-After), not_configured (this site's own wording for changes is not set up; no Retry-After) or no_worker (no service can make it now; Retry-After: 60). | With a Retry-After: try again after it. Without one (keyframe changes: not_enabled or not_configured): the feature is not offered here - do not retry, hide it in your program. (retryable says true for this code in both cases; go by Retry-After and details.reason.) |
InsufficientStorage | 507 | no | Your storage is full (or the server's disk), counting what your open uploads still need. | Delete media and jobs you do not need (download the results first), or open fewer uploads at once. |
