Skip to main content

How errors are returned

The Integrations API does not use a universal error_code field, and it does not return a request_id. Instead, errors are signalled by the HTTP status code and one of four response body shapes. Always branch on response.status_code first, then read the body shape that matches.

Error envelope shapes

Most domain errors return a detail message. Some also include a docs_url pointing at the relevant documentation.
{
  "detail": "Asset already submitted",
  "docs_url": "https://docs.mediamagic.app/concepts/assets"
}
The docs_url field is optional. Authentication failures (401) use this shape with messages such as Missing X-API-Key header, Invalid API key, This API key has expired, or This API key has been revoked.

Status codes

CodeWhen
200OK
201Created (submission, direct upload, webhook subscription)
202Accepted (asset retry)
204No content (delete subscription)
400simulationDelaySeconds sent on a live key; subscription missing a delivery destination
401Missing, invalid, expired, or revoked API key
402Tier not authorized for the Integrations API (plan gating)
403Sandbox-only endpoint called with a live key; invalid blob path
404Resource not found in your workspace; asset not finished (topics or issues still pending)
409Asset already submitted; asset not retryable; retry already in progress
413Upload too large
415Unsupported or mismatched content type
422Request validation failed
429Rate limit exceeded (includes Retry-After)
500Storage error or internal failure
502Failed to load an issue or topic blob from storage

Common situations

These are real conditions you will encounter, described by status code and the style of detail message returned. There are no error-code constants to match against.
StatusSituationExample message
401Missing or bad API keyInvalid API key
402Plan does not include API accesssee the 402 envelope above
403Invalid blob path, or sandbox-only endpoint hit with a live keyInvalid blob path
404Submission or asset not found in your workspace; topics/issues requested before the asset finishedSubmission not found
409Asset already submitted, or a retry is not allowedAsset already submitted
413File exceeds the direct-upload limitFile too large; use the presigned upload URL
415Content type not in the allowlist or does not match the fileUnsupported content type
422Request body failed validationsee the validation envelope above
429A rate-limit bucket was exceededRate limit exceeded: ...
500Storage write or internal errorInternal server error
502Issue or topic blob could not be loadedFailed to load results from storage

Retry guidance

StatusRetryable?How
429YesWait for the Retry-After header, then retry with backoff
500YesRetry with exponential backoff and jitter
502YesRetry with exponential backoff and jitter
Other 4xxNoFix the request; retrying will not help
Do not retry 400, 401, 402, 403, 404, 409, 413, 415, or 422 without changing the request. They indicate a problem with the request, the resource, or your plan that a retry alone cannot resolve.

Handling errors in code

Branch on the status code, then read the matching body shape. Read detail for domain errors and the fields array for validation errors. Do not look for error_code (except on 402) or request_id (which does not exist).
import requests


def handle_error(response: requests.Response) -> None:
    """Inspect an error response and decide what to do."""
    status = response.status_code

    try:
        body = response.json()
    except ValueError:
        print(f"HTTP {status}: {response.text}")
        return

    if status == 422 and body.get("error") == "validation_error":
        print(f"Validation failed: {body.get('message')}")
        for field in body.get("fields", []):
            print(f"  {field['field']}: {field['message']}")
    elif status == 402:
        print(f"Plan not authorized for bucket {body.get('bucket')}: {body.get('message')}")
    elif status == 429:
        print(body.get("error", "Rate limit exceeded"))
    else:
        # Domain error envelope.
        print(f"HTTP {status}: {body.get('detail', response.text)}")
        if body.get("docs_url"):
            print(f"See {body['docs_url']}")


def is_retryable(status_code: int) -> bool:
    """429, 500, and 502 are safe to retry with backoff."""
    return status_code in {429, 500, 502}
For 429 responses, wait for the duration in the Retry-After header before retrying. See Rate limiting for a complete backoff example.

Need help?