Overview
The Integrations API enforces rate limits to keep the service stable and usage fair. Limits are tier-based and resolved per API key: the limit that applies to a request depends on the plan associated with your key and on which capability bucket the request falls into.
Exact request-per-minute numbers are configured per tier and are not published here. To see the limits that apply to your key, check the control plane or contact support.
Capability buckets
Each request is counted against one of six buckets. Read operations, write operations, uploads, and sandbox calls are limited independently, so heavy polling does not consume your submission-write budget.
| Bucket | Covers |
|---|
upload | Presigned URL requests and direct uploads |
submission_write | Creating submissions and retrying assets |
submission_read | Reading submission status, assets, issues, topics, and versions |
webhook_write | Creating, updating, and deleting webhook subscriptions |
webhook_read | Listing subscriptions and reading delivery history |
sandbox | Sandbox-only endpoints such as firing synthetic events |
When you exceed a limit
Exceeding a bucket limit returns a 429 with a single error string and a Retry-After header (in seconds).
{
"error": "Rate limit exceeded: too many submission writes"
}
HTTP/1.1 429 Too Many Requests
Retry-After: 30
The API does not emit X-RateLimit-Limit, X-RateLimit-Remaining, or X-RateLimit-Reset headers. The only rate-limit header is Retry-After, returned on 429 responses. Do not write code that reads X-RateLimit-* headers.
When your plan lacks API access
If your tier is not authorized for the Integrations API at all, requests return a 402 instead of a 429. This is a plan-gating response, not a transient rate limit.
{
"error_code": "tier_not_authorized_for_integrations_api",
"message": "Your plan does not include access to the Integrations API.",
"bucket": "submission_write"
}
The bucket field names the capability that was gated. To enable API access, upgrade your plan in the control plane or contact support. Retrying a 402 will not succeed.
Best practices
Respect Retry-After
When you receive a 429, wait for the duration in the Retry-After header before retrying, then apply exponential backoff with jitter for any further attempts.
import random
import time
import requests
def request_with_backoff(method: str, url: str, headers: dict, max_retries: int = 5, **kwargs):
"""Retry on 429/500/502 with Retry-After and exponential backoff plus jitter."""
delay = 1.0
for attempt in range(max_retries):
response = requests.request(method, url, headers=headers, **kwargs)
if response.status_code not in (429, 500, 502):
return response
if response.status_code == 429 and response.headers.get("Retry-After"):
wait = float(response.headers["Retry-After"])
else:
wait = delay + random.uniform(0, delay) # exponential backoff with jitter
print(f"Got {response.status_code}, waiting {wait:.1f}s before retry...")
time.sleep(wait)
delay *= 2
raise RuntimeError("Exceeded max retries")
Batch assets into one submission
Submit all assets for a job in a single request rather than one submission per asset. This consumes a single submission_write instead of many.
# Less efficient: one submission per file.
for blob_path in blob_paths:
requests.post(
"https://mm-midmarket-integrations-api-preview.azurewebsites.net/api/integrations/submissions",
headers={"X-API-Key": api_key},
json={"assets": [{"blobPath": blob_path}]},
)
# Preferred: one submission for all files.
assets = [{"blobPath": blob_path} for blob_path in blob_paths]
requests.post(
"https://mm-midmarket-integrations-api-preview.azurewebsites.net/api/integrations/submissions",
headers={"X-API-Key": api_key},
json={"assets": assets},
)
Prefer webhooks over tight polling
Polling submission status in a tight loop consumes your submission_read budget quickly. Subscribe to webhooks so you are notified when processing completes, and poll only as a fallback. See Webhook subscriptions.
Poll with backoff
When you do poll, read status from GET /submissions/{id} (there is no /status suffix), space your requests out, and stop once the top-level status reaches a terminal state of complete or failed.
import time
import requests
API_BASE = "https://mm-midmarket-integrations-api-preview.azurewebsites.net"
def poll_until_done(submission_id: str, api_key: str) -> dict:
"""Poll a submission with a widening interval until it reaches a terminal state."""
start = time.time()
while True:
elapsed = time.time() - start
interval = 2 if elapsed < 30 else (10 if elapsed < 120 else 30)
response = requests.get(
f"{API_BASE}/api/integrations/submissions/{submission_id}",
headers={"X-API-Key": api_key},
)
submission = response.json()
if submission["status"] in ("complete", "failed"):
return submission
time.sleep(interval)
Queue and space out requests
If you generate bursts of work, place requests on a queue and release them at a steady rate rather than firing them all at once. This keeps you under your bucket limits and reduces the number of 429 responses you have to recover from.
Troubleshooting
Hitting limits unexpectedly
- Confirm you are batching assets into single submissions where possible
- Replace tight polling loops with webhooks
- Space requests out instead of issuing them concurrently
- Check your tier’s limits in the control plane
Getting 429 responses
- Wait for the
Retry-After duration before retrying
- Apply exponential backoff with jitter for repeated retries
- Remember that read, write, upload, and sandbox buckets are counted separately
Getting a 402 instead of a 429
A 402 with error_code: tier_not_authorized_for_integrations_api means your plan does not include API access. Upgrade in the control plane or contact support at https://support.mediamagic.app — retrying will not help.
Need higher limits?
Contact support at https://support.mediamagic.app with your expected request volume, use case, and timeline.