Skip to main content

How uploads work

Files are uploaded with a presigned URL: request a time-limited URL from the API, then upload the file straight to Azure Blob Storage. This keeps upload bandwidth off your API server, handles files up to the per-category maximums, and works well for parallel uploads.

Presigned URL uploads

Basic flow

1

Request presigned URL

Call /api/integrations/uploads/url to get a time-limited upload URL.
2

Upload directly to Azure

Use the returned URL to upload your file to Azure Blob Storage with a single PUT.
3

Create submission

Submit the returned blobPath in your submission request.
The request body uses filename and contentType. The response returns uploadUrl, blobPath, and expiresAt. The presigned URL expires after 1 hour.
This same upload flow is used for supporting context files, not just primary assets. Upload the file here, then reference its blobPath in a submission’s contextItems instead of assets. Document types (PDF, DOCX, TXT) are accepted in addition to media. See Tags and supporting context.

Implementation example

import requests
from pathlib import Path

API_KEY = "your-api-key-here"
API_BASE = "https://mm-midmarket-integrations-api-preview.azurewebsites.net"

def upload_file_with_presigned_url(file_path: str) -> str:
    """Upload a file using a presigned URL and return the blobPath."""

    # Step 1: Request a presigned URL
    file = Path(file_path)
    response = requests.post(
        f"{API_BASE}/api/integrations/uploads/url",
        headers={"X-API-Key": API_KEY},
        json={
            "filename": file.name,
            "contentType": get_content_type(file.suffix),
        },
    )
    response.raise_for_status()
    data = response.json()
    upload_url = data["uploadUrl"]
    blob_path = data["blobPath"]

    # Step 2: Upload to Azure with a single PUT
    with open(file_path, "rb") as f:
        upload_response = requests.put(
            upload_url,
            headers={"x-ms-blob-type": "BlockBlob"},
            data=f,
        )
        upload_response.raise_for_status()

    print(f"Uploaded {file.name} -> {blob_path}")
    return blob_path

def get_content_type(suffix: str) -> str:
    """Map a file extension to a supported content type."""
    types = {
        ".mp4": "video/mp4",
        ".mov": "video/quicktime",
        ".mp3": "audio/mpeg",
        ".wav": "audio/wav",
        ".jpg": "image/jpeg",
        ".png": "image/png",
        ".pdf": "application/pdf",
        ".txt": "text/plain",
    }
    return types.get(suffix.lower(), "application/octet-stream")

# Usage
blob_path = upload_file_with_presigned_url("meeting-recording.mp4")

Parallel uploads

For batch operations, request a presigned URL per file and upload them in parallel:
import asyncio
import aiohttp

API_KEY = "your-api-key-here"
API_BASE = "https://mm-midmarket-integrations-api-preview.azurewebsites.net"

async def request_presigned_url(session, file_name, content_type):
    async with session.post(
        f"{API_BASE}/api/integrations/uploads/url",
        headers={"X-API-Key": API_KEY},
        json={"filename": file_name, "contentType": content_type},
    ) as resp:
        resp.raise_for_status()
        return await resp.json()

async def upload_to_azure(session, upload_url, file_path):
    with open(file_path, "rb") as f:
        async with session.put(
            upload_url,
            headers={"x-ms-blob-type": "BlockBlob"},
            data=f,
        ) as resp:
            resp.raise_for_status()

async def upload_files_parallel(files: list[tuple[str, str]]) -> list[str]:
    """Upload multiple files in parallel. files = [(path, contentType), ...]."""
    blob_paths = []
    async with aiohttp.ClientSession() as session:
        # Request all presigned URLs first
        url_data = await asyncio.gather(
            *[
                request_presigned_url(session, path.split("/")[-1], ctype)
                for path, ctype in files
            ]
        )
        # Upload all files concurrently
        await asyncio.gather(
            *[
                upload_to_azure(session, data["uploadUrl"], path)
                for data, (path, _) in zip(url_data, files)
            ]
        )
        blob_paths = [data["blobPath"] for data in url_data]
    return blob_paths

Supported file types and sizes

The API accepts these MIME types:
CategoryAllowed content typesMax size
Videovideo/mp4, video/quicktime, video/x-msvideo, video/x-matroska5 GB
Audioaudio/mpeg, audio/wav, audio/x-wav, audio/aac, audio/ogg500 MB
Imageimage/jpeg, image/png, image/gif, image/webp, image/svg+xml50 MB
Documentapplication/pdf, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, text/plain100 MB
A 415 is returned when the contentType is not in this allowlist or does not match the file.

Handling large files

Large files use the same presigned URL flow. A single PUT works for the SAS URL. For very large files you can use the Azure Blob Storage SDK against the same SAS URL, which handles the transfer for you:
from azure.storage.blob import BlobClient

def upload_large_file(upload_url: str, file_path: str):
    """Upload a large file to the presigned SAS URL using the Azure SDK."""

    blob_client = BlobClient.from_blob_url(upload_url)

    with open(file_path, "rb") as f:
        blob_client.upload_blob(f, overwrite=True)

    print("Upload complete")
The presigned URL is valid for 1 hour. If the upload does not finish in that window, request a new URL and retry. There is no application-level resumable, multipart, or verify step — the upload completes when the PUT to Azure succeeds, and the blobPath is ready to submit.

Error handling

Network retries

Implement exponential backoff for transient failures:
import time
from requests.exceptions import RequestException

def upload_with_retries(file_path: str, max_retries: int = 3) -> str:
    """Upload with automatic retry on transient failure."""

    for attempt in range(max_retries):
        try:
            return upload_file_with_presigned_url(file_path)
        except RequestException:
            if attempt < max_retries - 1:
                wait_time = 2 ** attempt  # Exponential backoff
                print(f"Upload failed, retrying in {wait_time}s...")
                time.sleep(wait_time)
            else:
                raise

Validate before uploading

Check the file exists, is readable, and is within the category limit before you upload:
import os

def validate_before_upload(file_path: str, max_mb: int) -> bool:
    """Check file exists, is readable, and within the category size limit."""

    if not os.path.exists(file_path):
        raise FileNotFoundError(f"File not found: {file_path}")

    size_mb = os.path.getsize(file_path) / (1024 * 1024)
    if size_mb > max_mb:
        raise ValueError(f"File too large: {size_mb:.1f} MB (max {max_mb} MB)")

    if not os.access(file_path, os.R_OK):
        raise PermissionError(f"File not readable: {file_path}")

    return True

Troubleshooting

Upload URL expired

Presigned URLs expire after 1 hour. If the upload takes longer, request a new URL and retry.

Content type mismatch

Ensure the contentType matches the actual file format and is one of the allowed MIME types above. A mismatched or unsupported type returns 415.

403 invalid filename

The filename failed validation. Use a plain filename without path traversal or unusual characters.

413 payload too large

The uploaded blob exceeds the per-category size limit for its type. Check the file against the maximum sizes above before submitting.