API reference

API error codes

One error envelope, one stable code per failure, and a doc_url that lands on this page. 45 codes across 8 families — jump to a family, or search the page for the code you received.

The contract

One error object, returned the same way everywhere

Every failure — whichever API, whichever stage — is serialised by the same response builder. Clients parse one shape, write one handler.

Fields

FieldRequiredPurpose
typeRequiredCoarse family — one of the 8 listed below. Branch on this first.
codeRequiredStable machine-readable identifier in snake_case.
messageRequiredHuman-readable English sentence that says how to recover.
paramOptionalNames the offending field so a form or SDK can highlight it.
request_idRequiredEnd-to-end trace ID, identical to the X-Request-Id response header.
doc_urlRequiredDeep link to this page, anchored on the exact code.
retryableRequiredBoolean the SDK uses to decide whether to retry automatically.
detailsOptionalStructured extras, e.g. required_credits and available_credits.
JSON
{
  "error": {
    "type": "billing_error",
    "code": "insufficient_credits",
    "message": "Not enough credits for this job. Required 120, available 45.",
    "param": null,
    "request_id": "req_01J8ZK3M9QX4HD2N9R",
    "doc_url": "https://mlslabs.io/docs/errors.html#insufficient_credits",
    "retryable": false,
    "details": {
      "required_credits": 120,
      "available_credits": 45
    }
  }
}

Why code is frozen and message is not. Clients are only ever expected to branch on code; message is prose and may be reworded at any time. A published code never changes meaning — new codes get added, existing ones are marked deprecated and honoured for at least 12 months.

Same shape in three places

Sync response, async job, webhook

Polling and webhooks carry the identical object, so a failure handler written once covers the whole async job model.

GET /v1/sdr2hdr/jobs/job_01J8ZK3M9QX4HD2N7P — job finished
{
  "job_id": "job_01J8ZK3M9QX4HD2N7P",
  "status": "failed",
  "error": {
    "type": "billing_error",
    "code": "insufficient_credits",
    "message": "Not enough credits for this job. Required 120, available 45.",
    "param": null,
    "request_id": "req_01J8ZK3M9QX4HD2N9R",
    "doc_url": "https://mlslabs.io/docs/errors.html#insufficient_credits",
    "retryable": false,
    "details": {
      "required_credits": 120,
      "available_credits": 45
    }
  },
  "billing": {
    "estimated_credits": 120,
    "charged_credits": 0,
    "refunded": true
  }
}
POST to your webhook — event job.failed
{
  "event": "job.failed",
  "created_at": "2026-09-11T02:00:00Z",
  "data": {
    "job": {
      "job_id": "job_01J8ZK3M9QX4HD2N7P"
    },
    "error": {
      "type": "billing_error",
      "code": "insufficient_credits",
      "message": "Not enough credits for this job. Required 120, available 45.",
      "param": null,
      "request_id": "req_01J8ZK3M9QX4HD2N9R",
      "doc_url": "https://mlslabs.io/docs/errors.html#insufficient_credits",
      "retryable": false,
      "details": {
        "required_credits": 120,
        "available_credits": 45
      }
    }
  }
}

All codes

45 error codes in 8 families

Each code below is a permanent anchor — link to any row directly, e.g. /docs/errors.html#insufficient_credits. That is exactly the URL returned in doc_url.

Authentication & authorization

auth_errorHTTP 401 / 403

The credential is missing, wrong, or not allowed to call this API. Nothing to retry — the caller has to send a working key. Kept strictly apart from billing errors below.

CodeHTTPWhen it happensWhat to doSDK auto-retry
missing_api_key401No X-API-Key header on the requestSend the key: -H "X-API-Key: mls_live_..."No
invalid_api_key401The key does not exist or is malformedRe-copy the key from the dashboard and check for stray whitespaceNo
expired_api_key401The key passed its expiry dateRotate the key in the dashboardNo
revoked_api_key401The key was revoked manually or by a leak scanIssue a new key — the old one stays deadNo
insufficient_scope403The key is not scoped to this API or projectUse a key whose scope covers this endpointNo
account_suspended403The account was suspended for abuse or an unpaid balanceContact supportNo

Billing & credits

billing_errorHTTP 402

The request itself is fine — the account cannot pay for it. Always HTTP 402, never 401, so client dashboards can route the user to top-up instead of key rotation.

CodeHTTPWhen it happensWhat to doSDK auto-retry
insufficient_credits402Balance is below the estimated cost of the jobdetails carries the exact shortfall — top up and resendNo
spend_limit_exceeded402The monthly spend cap was reachedRaise the cap or wait for the next billing periodNo
plan_not_supported402The plan does not include this API, resolution or tierUpgrade the plan for the featureNo
payment_failed402The card on file was declined or has expiredUpdate the payment methodNo
subscription_inactive402The subscription lapsedReactivate the subscriptionNo

Rate limits & concurrency

rate_limit_errorHTTP 429 / 503

More work in flight than the tier allows. These are safe to repeat: the SDK backs off automatically and honours the Retry-After header.

CodeHTTPWhen it happensWhat to doSDK auto-retry
rate_limit_exceeded429Requests per second above the tier limitBack off for Retry-After seconds, then resendYes
concurrency_limit_exceeded429More jobs running at once than the tier allowsWait for an in-flight job to finish, then resubmitYes
quota_exceeded429The period quota is used up (e.g. monthly processing minutes)Upgrade the tier or wait for the quota to resetNo
queue_full503The processing queue is saturatedRetry with exponential backoffYes

Request & media

invalid_request_errorHTTP 400 / 413 / 415 / 422

The request reached the API but the payload is wrong. param names the offending field so forms and SDKs can highlight it — fix it and resend, since repeating the same body will fail the same way.

CodeHTTPWhen it happensWhat to doSDK auto-retry
missing_parameter400A required field was not sentSend the field named in paramNo
invalid_parameter400A value is out of range or the wrong typeCorrect the value against the API referenceNo
invalid_url400The source URL is unreachable or its signature expiredRegenerate a fresh signed URLNo
duration_exceeded400The input is longer than this API acceptsSplit the media or move to a higher tierNo
file_too_large413The payload is above the size limitUse an object-storage URL instead of a direct uploadNo
unsupported_format415The container format is not supportedTranscode to MP4, MOV or WAV and resendNo
unsupported_codec415The codec inside the container is not supportedTranscode the video or audio stream and resendNo
invalid_media422The file is corrupt or cannot be decodedCheck the source file plays locally firstNo
content_policy_violation422The content breaches the usage policyReplace the asset — do not retryNo

Resources & jobs

resource_errorHTTP 404 / 409

The resource is missing, or the request conflicts with the current state of a job.

CodeHTTPWhen it happensWhat to doSDK auto-retry
job_not_found404The job_id does not exist or aged out of retentionKeep the ID from the submit response and re-check itNo
asset_not_found404The referenced output asset or upload is goneRe-run the job or re-upload the assetNo
job_not_cancellable409The job already finished or is past the cancel windowStop cancelling and read the final job statusNo
idempotency_conflict409The same Idempotency-Key was reused with a different bodySend a new key, or resend the identical bodyNo
resource_locked409The resource is in use by another jobRetry once the other job completesYes

Processing failures

processing_errorHTTP 422 / 500 / 502 / 504

The job was accepted and then failed inside the pipeline. These arrive on GET /jobs/{id} with status: "failed" and in the job.failed webhook, using the same envelope.

CodeHTTPWhen it happensWhat to doSDK auto-retry
processing_failed422Generic pipeline failure with no more specific causeRetry once — if it persists, send the request_id to supportNo
out_of_memory500Resolution or duration exceeds the worker configurationLower the target resolution or split the inputNo
upstream_error502A dependency behind the API failedRetry after a short delayYes
timeout504Processing exceeded the job time limitRetry, or split into smaller jobsYes
drm_protected422The input is DRM-protected and cannot be decodedSupply a DRM-free sourceNo
no_speech_detected422No speech found in a subtitle or audio jobCheck the audio track carries usable dialogueNo
job_cancelled409The job was cancelled by you or by the systemNo action — stop polling this jobNo

Service availability

server_errorHTTP 500 / 503

Our side of the wire. Retry with backoff; if it keeps failing, open a ticket with the request_id so the call can be traced end to end.

CodeHTTPWhen it happensWhat to doSDK auto-retry
internal_error500An unexpected exception inside the APIReport it with the request_idYes
service_unavailable503The service is temporarily unavailableRetry with exponential backoffYes
maintenance_window503Planned maintenanceWait for the maintenance window to closeYes
region_unavailable503The target region is degradedRetry against another regionYes

UE Plugin licensing

license_errorHTTP 402 / 403

Returned by the 3DGS/4DGS UE Plugin licence endpoint, so the plugin can show a precise message in the editor instead of failing silently.

CodeHTTPWhen it happensWhat to doSDK auto-retry
license_invalid403The licence key is not recognisedSign out and activate againNo
license_expired403The licence term endedRenew to restore watermark-free outputNo
activation_limit_reached403The plan's activated-device count is exhaustedDeactivate an old machine, then activate this oneNo
device_not_activated403This machine has not been activated yetRun the activation flow in the plugin panelNo
offline_grace_expired403The offline grace period ended and online validation is dueConnect to the internet once to restore itYes

HTTP status

Status codes and what they mean

The status tells you who has to act; the code tells you exactly what to change.

StatusMeaningRetry
400Malformed request or bad parameterNo — fix the payload
401Not authenticated / credential invalidNo — send a working key
402Payment required / out of creditsNo — top up, then resend
403Authenticated but not permitted / account restrictedNo
404Resource does not existNo
409Conflicts with current resource stateDepends on the code
413Payload too largeNo — use object storage
415Media type not supportedNo — transcode first
422Semantically invalid / processing failedFollow retryable
429Rate limit or concurrency limit hitYes — honour Retry-After
500Internal error on our sideYes — with backoff
502 / 504Upstream failure / processing timeoutYes — with backoff
503Service unavailable or queue saturatedYes — with backoff

401 and 402 are deliberately kept apart: a bad credential and an empty balance are different user actions, and collapsing them into one status is the single most common API design mistake.

Retries

What the SDK does for you

Every official SDK reads retryable from the envelope instead of guessing from the status code. When a retry is safe it also replays the original Idempotency-Key, so a network hiccup can never bill the same job twice.

  • Typed exceptions per family — no string parsing
  • Automatic backoff that respects Retry-After
  • Idempotency keys carried through retries
  • request_id surfaced on every exception for support
Back to the docs
SituationSDK behaviour
retryable: true with 429 / 503 / 504Exponential backoff, up to 3 attempts, always honouring Retry-After
retryable: true with 500 / 502Exponential backoff, up to 2 attempts
retryable: falseNo retry — the SDK raises a typed exception immediately
Network timeout with no responseRetried, but sends the same Idempotency-Key so the job is never charged twice
processing_failed on a finished async jobNever retried automatically — the business layer decides

Billing

Failed jobs are never charged

Errors and credits are bound together, because a charge for work that never happened costs far more trust than the credits are worth.

  • Pre-authorise, then settle. Submitting freezes the estimated credits; success settles the real usage.
  • Failure releases everything. A failed job reports charged_credits: 0 and refunded: true in the job response and the webhook.
  • Partial failures settle pro rata, with the per-item breakdown in details.
  • Quota errors always carry numbers, so the UI can render the exact shortfall instead of a generic message.
Money, not magic
# insufficient_credits always quantifies the gap
need = err.details["required_credits"] - err.details["available_credits"]
ui.show("Top up " + str(need) + " credits to run this job")

# a finished-but-failed job is free
job = client.sdr2hdr.get(job_id)
assert job.billing.charged_credits == 0
assert job.billing.refunded is True

Try it in Postman

Saved 401, 402 and 429 responses

The collection ships these six saved examples, so you can see each envelope in the Postman response pane — and against a mock server — before writing a single line of client code.

401 · invalid_api_key
GET /v1/account/credits

HTTP/1.1 401 Unauthorized
X-Request-Id: req_01J8ZK3M9QX4HD2N7P
Content-Type: application/json

{
  "error": {
    "type": "auth_error",
    "code": "invalid_api_key",
    "message": "The API key is not valid. Check the X-API-Key header or rotate the key in the dashboard.",
    "param": null,
    "request_id": "req_01J8ZK3M9QX4HD2N7P",
    "doc_url": "https://mlslabs.io/docs/errors.html#invalid_api_key",
    "retryable": false
  }
}
401 · missing_api_key
GET /v1/account/credits

HTTP/1.1 401 Unauthorized
X-Request-Id: req_01J8ZK3M9QX4HD2N8Q
Content-Type: application/json

{
  "error": {
    "type": "auth_error",
    "code": "missing_api_key",
    "message": "No API key was supplied. Send it in the X-API-Key header.",
    "param": null,
    "request_id": "req_01J8ZK3M9QX4HD2N8Q",
    "doc_url": "https://mlslabs.io/docs/errors.html#missing_api_key",
    "retryable": false
  }
}
402 · insufficient_credits
POST /v1/sdr2hdr/jobs

HTTP/1.1 402 Payment Required
X-Request-Id: req_01J8ZK3M9QX4HD2N9R
Content-Type: application/json

{
  "error": {
    "type": "billing_error",
    "code": "insufficient_credits",
    "message": "Not enough credits for this job. Required 120, available 45.",
    "param": null,
    "request_id": "req_01J8ZK3M9QX4HD2N9R",
    "doc_url": "https://mlslabs.io/docs/errors.html#insufficient_credits",
    "retryable": false,
    "details": {
      "required_credits": 120,
      "available_credits": 45
    }
  }
}
402 · spend_limit_exceeded
POST /v1/sdr2hdr/jobs

HTTP/1.1 402 Payment Required
X-Request-Id: req_01J8ZK3M9QX4HD2NAR
Content-Type: application/json

{
  "error": {
    "type": "billing_error",
    "code": "spend_limit_exceeded",
    "message": "Monthly spend limit reached. This job would exceed the cap on your account.",
    "param": null,
    "request_id": "req_01J8ZK3M9QX4HD2NAR",
    "doc_url": "https://mlslabs.io/docs/errors.html#spend_limit_exceeded",
    "retryable": false,
    "details": {
      "spend_limit_usd": 700,
      "spent_usd": 700,
      "reset_at": "2026-10-01T00:00:00Z"
    }
  }
}
429 · rate_limit_exceeded
POST /v1/sdr2hdr/jobs

HTTP/1.1 429 Too Many Requests
X-Request-Id: req_01J8ZK3M9QX4HD2NBR
Retry-After: 2
Content-Type: application/json

{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. This plan allows 20 requests per second.",
    "param": null,
    "request_id": "req_01J8ZK3M9QX4HD2NBR",
    "doc_url": "https://mlslabs.io/docs/errors.html#rate_limit_exceeded",
    "retryable": true,
    "details": {
      "limit": 20,
      "window_seconds": 1,
      "retry_after_seconds": 2
    }
  }
}
429 · concurrency_limit_exceeded
POST /v1/sdr2hdr/jobs

HTTP/1.1 429 Too Many Requests
X-Request-Id: req_01J8ZK3M9QX4HD2NCR
Retry-After: 5
Content-Type: application/json

{
  "error": {
    "type": "rate_limit_error",
    "code": "concurrency_limit_exceeded",
    "message": "Too many jobs running at once. This plan allows 8 concurrent jobs.",
    "param": null,
    "request_id": "req_01J8ZK3M9QX4HD2NCR",
    "doc_url": "https://mlslabs.io/docs/errors.html#concurrency_limit_exceeded",
    "retryable": true,
    "details": {
      "concurrency_limit": 8,
      "running_jobs": 8
    }
  }
}

Collection file: mls-api.postman_collection.json · import it into Postman and set the api_key collection variable.

Questions

Common questions about API errors

Does an invalid API key return 401 or 402?

401. invalid_api_key means the credential is wrong, so the fix is to rotate the key. insufficient_credits returns 402 because the credential is fine and the action is to top up. Mixing the two sends customers down the wrong path.

Are credits charged when a job fails?

No. Credits are pre-authorised on submit and settled only on success — a failed job refunds the full authorisation and reports charged_credits: 0 with refunded: true.

Is the error object the same for sync calls, async jobs and webhooks?

Yes. The same error object with the same fields is returned by the synchronous response, by GET /jobs/{id} when status is failed, and inside the job.failed webhook — so one handler covers all three.

Why did I get a different-looking error from the gateway?

Requests rejected at the API gateway — throttling or credential checks that never reach the backend — used to be serialised by the gateway itself. The gateway now rewrites those bodies into the same envelope, so every 401 and 429 is indistinguishable in shape from a backend error.

Handle failures once, everywhere

One envelope across sync calls, async jobs and webhooks — with credits refunded automatically when a job fails.