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. 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.
Most domain errors return a detail message. Some also include an optional docs_url.
{
  "detail": "Asset already submitted",
  "docs_url": "https://docs.mediamagic.app/concepts/assets"
}
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.

HTTP status codes

CodeMeaningRetryable?
200OKNo
201Created (submission, direct upload, webhook subscription)No
202Accepted (asset retry)No
204No content (delete subscription)No
400Bad request (e.g. simulationDelaySeconds on a live key)No
401Missing, invalid, expired, or revoked API keyNo
402Tier not authorized for the Integrations APINo
403Sandbox-only endpoint with a live key; invalid blob pathNo
404Resource not found in your workspaceNo
409Asset already submitted; asset not retryableNo
413Upload too largeNo
415Unsupported or mismatched content typeNo
422Request validation failedNo
429Rate limit exceeded (includes Retry-After)Yes (with backoff)
500Storage error or internal failureYes (with backoff)
502Failed to load an issue or topic blob from storageYes (with backoff)
Only 429, 500, and 502 are safe to retry. Every other 4xx indicates a problem with the request, the resource, or your plan that a retry alone cannot resolve.

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.

401 Invalid API key

Missing or invalid API key, returned as a domain error.
{
  "detail": "Invalid API key"
}
Check that the X-API-Key header is present and correct, and that the key has not expired or been revoked.

403 Invalid blob path

A blob path failed validation, or a sandbox-only endpoint was called with a live key.
{
  "detail": "Invalid blob path"
}

404 Not found

A submission or asset does not exist in your workspace, or topics/issues were requested before the asset finished processing.
{
  "detail": "Submission not found"
}

409 Asset already submitted

The same asset was already submitted, or a retry is not allowed.
{
  "detail": "Asset with blobPath '<workspace-id>/campaign-video.mp4' already submitted"
}
To process the file again, check /api/integrations/submissions/{submission_id}/versions for existing versions, or upload a new file.

413 Upload too large

The file exceeds the direct-upload limit. Use the presigned upload URL flow instead.
{
  "detail": "File too large; use the presigned upload URL"
}

415 Unsupported content type

The content type is not in the allowlist, or it does not match the file.
{
  "detail": "Unsupported content type"
}

422 Validation error

The request body failed validation. Read the fields array — see the validation envelope above.

402 Tier not authorized

Your plan does not include Integrations API access — see the 402 envelope above.

429 Rate limit exceeded

A rate-limit bucket was exceeded. Wait for the Retry-After header — see the 429 envelope above.

Retry strategies

Retry only on 429, 500, and 502. Use exponential backoff with jitter, and respect the Retry-After header on 429 responses.

Exponential backoff with jitter

import random
import time

import requests

API_BASE = "https://mm-midmarket-integrations-api-preview.azurewebsites.net"
RETRYABLE = {429, 500, 502}


def request_with_retries(
    method: str,
    url: str,
    max_retries: int = 4,
    base_delay: float = 1.0,
    **kwargs,
) -> requests.Response:
    """Make a request, retrying only 429/500/502 with backoff and jitter."""
    for attempt in range(max_retries):
        response = requests.request(method, url, timeout=10, **kwargs)

        if response.status_code not in RETRYABLE or attempt == max_retries - 1:
            return response

        # Honour Retry-After (seconds) on 429, otherwise back off exponentially.
        retry_after = response.headers.get("Retry-After")
        if response.status_code == 429 and retry_after:
            delay = float(retry_after)
        else:
            delay = base_delay * (2 ** attempt)
            delay += random.uniform(0, delay * 0.1)  # jitter

        print(f"HTTP {response.status_code}, retry {attempt + 1}/{max_retries} in {delay:.1f}s")
        time.sleep(delay)

    raise RuntimeError(f"Failed after {max_retries} attempts")


response = request_with_retries(
    "POST",
    f"{API_BASE}/api/integrations/submissions",
    headers={"X-API-Key": "your-api-key-here"},
    json={"assets": [{"blobPath": "<workspace-id>/campaign-video.mp4"}]},
)
For 429 responses, the Retry-After header gives the number of seconds to wait. See Rate limiting for the bucket model.

A note on duplicate submissions

Submission creation is not idempotent, and the API does not support an Idempotency-Key header. If you retry a submission request whose response you did not receive, you may create a duplicate. Guard against this client-side — for example, only retry submission creation on a network error or a retryable status (429/500/502), and track which assets you have already submitted. A repeated submission of the same asset returns 409 (asset already submitted).

Validation before upload

Validate files client-side before uploading to fail fast. Maximum sizes are per category (video up to 5 GB, audio 500 MB, image 50 MB, document 100 MB); the 10 MB cap applies only to the direct-upload endpoint. The example below enforces per-category limits.
import mimetypes
import os

ALLOWED_TYPES = {
    "video/mp4",
    "video/quicktime",
    "video/x-msvideo",
    "video/x-matroska",
    "audio/mpeg",
    "audio/wav",
    "audio/x-wav",
    "audio/aac",
    "audio/ogg",
    "image/jpeg",
    "image/png",
    "image/gif",
    "image/webp",
    "image/svg+xml",
    "application/pdf",
    "application/msword",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "text/plain",
}

# Maximum file size per category, in bytes.
MAX_SIZES = {
    "video": 5 * 1024**3,
    "audio": 500 * 1024**2,
    "image": 50 * 1024**2,
    "application": 100 * 1024**2,
    "text": 100 * 1024**2,
}


def validate_file(file_path: str) -> str:
    """Validate a file before upload. Returns the detected MIME type."""
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"File not found: {file_path}")
    if not os.access(file_path, os.R_OK):
        raise PermissionError(f"File not readable: {file_path}")

    mime_type, _ = mimetypes.guess_type(file_path)
    if mime_type not in ALLOWED_TYPES:
        raise ValueError(f"Unsupported content type: {mime_type}")

    category = mime_type.split("/", 1)[0]
    max_size = MAX_SIZES.get(category)
    size = os.path.getsize(file_path)
    if max_size and size > max_size:
        raise ValueError(f"File too large: {size} bytes (max {max_size} for {category})")

    return mime_type


try:
    validate_file("campaign-video.mp4")
except (FileNotFoundError, PermissionError, ValueError) as exc:
    print(f"Validation failed: {exc}")

Logging errors

Log the status code and the detail (or validation fields) for context. Do not log error_code or request_id — neither exists on most responses.
import logging

import requests

logger = logging.getLogger(__name__)


def log_api_error(response: requests.Response) -> None:
    """Log an API error with the status code and message."""
    try:
        body = response.json()
    except ValueError:
        body = {}

    detail = body.get("detail") or body.get("message") or body.get("error")

    logger.error(
        "Integrations API error",
        extra={
            "status_code": response.status_code,
            "detail": detail,
            "fields": body.get("fields"),
            "url": response.url,
            "method": response.request.method,
        },
    )


response = requests.post("...")
if response.status_code >= 400:
    log_api_error(response)

Handling specific asset errors

Per-asset failures are reported by GET /api/integrations/submissions/{submission_id} (there is no /status suffix). Each asset carries flat camelCase failure fields: errorMessage, errorType, isRetryable, and retryCount. There is no nested error object. An asset is failed when its status is failed; terminal asset states are complete and failed.
import requests

API_BASE = "https://mm-midmarket-integrations-api-preview.azurewebsites.net"


def get_failed_assets(submission_id: str, api_key: str) -> list[dict]:
    """Return the assets that failed processing."""
    response = requests.get(
        f"{API_BASE}/api/integrations/submissions/{submission_id}",
        headers={"X-API-Key": api_key},
        timeout=10,
    )
    response.raise_for_status()
    submission = response.json()

    failed = [
        asset for asset in submission["assets"]
        if asset.get("status") == "failed"
    ]

    for asset in failed:
        message = asset.get("errorMessage")
        error_type = asset.get("errorType")
        retryable = asset.get("isRetryable", False)
        print(f"{asset['assetId']}: {error_type} - {message} (retryable={retryable})")

    return failed
If isRetryable is true, you can re-run the asset with POST /api/integrations/submissions/{submission_id}/assets/{asset_id}/retry.

Circuit breaker pattern

For high-volume production systems, wrap API calls in a circuit breaker so repeated failures fail fast instead of piling up.
import requests
from circuitbreaker import circuit

API_BASE = "https://mm-midmarket-integrations-api-preview.azurewebsites.net"


@circuit(failure_threshold=5, recovery_timeout=60)
def call_integrations_api(endpoint: str, **kwargs) -> dict:
    """Call the API with a circuit breaker."""
    response = requests.get(f"{API_BASE}{endpoint}", timeout=10, **kwargs)
    response.raise_for_status()
    return response.json()


submission_id = "550e8400-e29b-41d4-a716-446655440000"
try:
    submission = call_integrations_api(
        f"/api/integrations/submissions/{submission_id}",
        headers={"X-API-Key": "your-api-key-here"},
    )
    print(f"Status: {submission['status']} ({submission['submissionId']})")
except Exception as exc:
    print(f"Request failed or circuit open: {exc}")

Error handling checklist

  • Branch on response.status_code, then read detail (or fields for 422 validation errors)
  • Retry only 429, 500, and 502 — never other 4xx
  • Respect the Retry-After header on 429 responses
  • Use exponential backoff with jitter for retries
  • Validate files client-side before uploading
  • Log the status code and message with full context
  • Guard against duplicate submissions client-side (there is no idempotency key)
  • Implement a circuit breaker for high-volume systems