Skip to main content

Choosing your upload method

Use presigned URLs if:
  • File size is larger than 10 MB
  • You’re uploading many files in parallel
  • You want better performance and scalability
  • Your API server has limited resources
Use direct uploads if:
  • File size is 10 MB or smaller
  • Simplicity is more important than performance
  • You’re uploading a single file at a time
  • You want a one-step process
The direct upload endpoint streams through the API and is capped at 10 MB. Anything larger must go through a presigned URL straight to Azure Blob Storage.

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.

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

Direct uploads

Basic flow

1

Stream file to the API

POST your file to /api/integrations/uploads/direct as multipart/form-data with the field name file.
2

Receive blob path

Get back the blobPath (plus sizeBytes and contentType) immediately.
3

Create submission

Use the blobPath in your submission request.
The direct endpoint returns 201 and is limited to 10 MB. Files larger than that return 413 with a message pointing you to the presigned URL flow.

Implementation

import requests

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

def upload_file_direct(file_path: str) -> str:
    """Upload a file (10 MB max) directly through the API."""

    with open(file_path, "rb") as f:
        response = requests.post(
            f"{API_BASE}/api/integrations/uploads/direct",
            headers={"X-API-Key": "your-api-key-here"},
            files={"file": f},
        )

    response.raise_for_status()
    return response.json()["blobPath"]

# Usage
blob_path = upload_file_direct("small-file.pdf")

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. The 10 MB cap applies only to the direct-upload streaming endpoint, not to the overall file size limits above.

Handling large files

Files larger than 10 MB use a presigned URL. Request the URL from /api/integrations/uploads/url, then PUT the file directly to Azure Blob Storage using the returned uploadUrl. 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_direct(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 direct upload endpoint accepts files up to 10 MB. For larger files, switch to the presigned URL flow.