Media Quick Start

This guide gets you uploading images to Mushu Media in under 5 minutes. You'll learn both CLI and API approaches.

First time? Complete the Quick Start to set up CLI, organization, and API keys.

Option 1: CLI Upload

The fastest way to upload an image:

mushu media upload photo.jpg --org ORG_ID

The CLI will:

  1. Request an upload URL from Mushu
  2. Upload your file directly to Cloudflare R2
  3. Confirm the upload and trigger processing
  4. Return the image URL and variants

Output:

{
  "media_id": "media_abc123",
  "status": "ready",
  "url": "https://images.mushucorp.com/t/original/org_xxx/media_abc123/photo.jpg",
  "variants": {
    "thumbnail": "https://images.mushucorp.com/t/thumbnail/...",
    "small": "https://images.mushucorp.com/t/small/...",
    "medium": "https://images.mushucorp.com/t/medium/...",
    "large": "https://images.mushucorp.com/t/large/..."
  }
}

Option 2: curl Upload

For API integration, here's the 4-step flow using curl.

Step 1: Get an Upload URL

With an API key (server-to-server):

curl -X POST https://media.mushucorp.com/media/upload-url \
  -H "X-API-Key: msk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "org_id": "org_xxx",
    "filename": "photo.jpg",
    "content_type": "image/jpeg",
    "size_bytes": 102400
  }'

Or with a user session token:

curl -X POST https://media.mushucorp.com/media/upload-url \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "org_id": "org_xxx",
    "filename": "photo.jpg",
    "content_type": "image/jpeg",
    "size_bytes": 102400
  }'

Response:

{
  "media_id": "media_abc123",
  "upload_url": "https://xxx.r2.cloudflarestorage.com/...",
  "key": "org_xxx/media_abc123/photo.jpg",
  "expires_in": 3600
}

Step 2: Upload the File

Use the upload_url from the previous response:

curl -X PUT "UPLOAD_URL_FROM_STEP_1" \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg

Note: This request goes directly to Cloudflare R2, not to the Mushu API. No authorization header is needed—the presigned URL includes the credentials.

Step 3: Confirm the Upload

Tell Mushu the upload is complete so it can process the image:

curl -X POST https://media.mushucorp.com/media/media_abc123/confirm \
  -H "X-API-Key: msk_live_YOUR_KEY"

Response:

{
  "status": "ready",
  "url": "https://images.mushucorp.com/t/original/org_xxx/media_abc123/photo.jpg"
}

Step 4: Get Image Variants

Retrieve all available image sizes:

curl https://media.mushucorp.com/media/media_abc123/images \
  -H "X-API-Key: msk_live_YOUR_KEY"

Response:

{
  "original": "https://images.mushucorp.com/t/original/...",
  "thumbnail": "https://images.mushucorp.com/t/thumbnail/...",
  "small": "https://images.mushucorp.com/t/small/...",
  "medium": "https://images.mushucorp.com/t/medium/...",
  "large": "https://images.mushucorp.com/t/large/..."
}

Python SDK

Install the SDK:

pip install mushu

Upload using the SDK:

import asyncio
import os

import httpx
import mushu
from mushu.media.api.media import get_upload_url, confirm_upload, get_image_urls
from mushu.media.models import UploadUrlRequest

# Create client with API key
media = mushu.client("media", api_key=os.environ["MUSHU_API_KEY"])

async def upload_image(file_path: str, org_id: str) -> dict:
    """Upload an image and return the media URLs."""
    filename = os.path.basename(file_path)
    file_size = os.path.getsize(file_path)

    # Determine content type
    ext = filename.lower().split(".")[-1]
    content_types = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png"}
    content_type = content_types.get(ext, "application/octet-stream")

    # Step 1: Get upload URL
    upload_data = await get_upload_url.asyncio(
        client=media,
        body=UploadUrlRequest(
            org_id=org_id,
            filename=filename,
            content_type=content_type,
            size_bytes=file_size,
        ),
    )

    # Step 2: Upload to presigned URL (direct to R2)
    async with httpx.AsyncClient() as http:
        with open(file_path, "rb") as f:
            await http.put(
                upload_data.upload_url,
                headers={"Content-Type": content_type},
                content=f.read(),
            )

    # Step 3: Confirm upload
    await confirm_upload.asyncio(client=media, media_id=upload_data.media_id)

    # Step 4: Get variants
    images = await get_image_urls.asyncio(client=media, media_id=upload_data.media_id)

    return {"media_id": upload_data.media_id, "images": images}

async def main():
    result = await upload_image("photo.jpg", "org_xxx")
    print(f"Media ID: {result['media_id']}")
    print(f"Images: {result['images']}")

asyncio.run(main())

Image Variants

Mushu automatically generates these variants for each uploaded image:

VariantMax DimensionUse Case
thumbnail150pxLists, grids, previews
small320pxMobile feeds
medium640pxDetail views, cards
large1280pxFull-screen mobile, web
originalOriginal sizeDownloads, high-res

Supported Formats

  • JPEG, PNG, GIF, WebP
  • Maximum file size: 10MB

Next Steps

Troubleshooting

403 Forbidden on upload

The presigned URL has expired. URLs are valid for 1 hour. Request a new upload URL and try again.

Image not processing

Make sure you called the confirm endpoint after uploading. Without confirmation, Mushu doesn't know the upload is complete.

Unsupported content type

Check that you're uploading a supported format (JPEG, PNG, GIF, WebP) and that the content_type in your request matches the actual file.