Media API
Upload images and videos to use in your posts. Schedulala supports three upload methods: presigned URLs for large files (up to 1 GB video / 20 MB images), simple multipart upload for small files (under 5 MB), and server-side upload from a public URL.
Endpoints
/api/v1/media/upload-urlGet presigned upload URL (large files)/api/v1/mediaSimple multipart upload (small files)/api/v1/media/from-urlRe-host a public URL on the Schedulala CDN/api/v1/mediaList your recent uploads (Media library + API)/api/v1/media/:id/confirmConfirm presigned upload completedGet presigned upload URL
/api/v1/media/upload-urlRequest a presigned URL for uploading large files directly to storageUse this endpoint for files larger than 5 MB (up to 1 GB for video, 20 MB for images). The upload flow has three steps:
Request a presigned URL
POST to /api/v1/media/upload-url with file metadata. You'll receive an upload URL, method, and headers.
Upload the file directly
PUT the file to the presigned URL using the provided headers. This uploads directly to storage — the file never passes through the Schedulala API.
Confirm the upload
POST to /api/v1/media/:id/confirm to mark the media as ready for use in posts.
Step 1: Request presigned URL
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
fileName | string | Required | Original file name (e.g., "product-demo.mp4") |
mimeType | string | Required | MIME type of the file (e.g., "video/mp4", "image/png") |
sizeBytes | number | Required | File size in bytes |
altText | string | Optional | Accessibility text for the media (recommended for images) |
fileNamestringRequiredOriginal file name (e.g., "product-demo.mp4")
mimeTypestringRequiredMIME type of the file (e.g., "video/mp4", "image/png")
sizeBytesnumberRequiredFile size in bytes
altTextstringOptionalAccessibility text for the media (recommended for images)
curl -X POST https://schedulala.com/api/v1/media/upload-url \-H "Authorization: Bearer sk_live_abc123xyz789..." \-H "Content-Type: application/json" \-d '{"fileName": "product-demo.mp4","mimeType": "video/mp4","sizeBytes": 52428800,"altText": "Product demo walkthrough"}'
{"success": true,"media": {"id": "media_abc123","uploadUrl": "https://storage.schedulala.com/uploads/media_abc123?X-Amz-Signature=...","method": "PUT","headers": {"Content-Type": "video/mp4"},"expiresIn": 3600}}
Step 2: Upload to presigned URL
Use the returned uploadUrl, method, and headers to upload the file directly to storage:
curl -X PUT "https://storage.schedulala.com/uploads/media_abc123?X-Amz-Signature=..." \-H "Content-Type: video/mp4" \--data-binary @product-demo.mp4
Step 3: Confirm upload
After the file has been uploaded to storage, confirm the upload so the media is marked as ready:
curl -X POST https://schedulala.com/api/v1/media/media_abc123/confirm \-H "Authorization: Bearer sk_live_abc123xyz789..."
{"success": true,"media": {"id": "media_abc123","url": "https://storage.schedulala.com/media/media_abc123.mp4","status": "ready","type": "video","mimeType": "video/mp4","size": 52428800,"altText": "Product demo walkthrough","expiresAt": "2026-03-31T12:00:00.000Z"}}
Simple multipart upload
/api/v1/mediaUpload small files directly via multipart form dataFor files under 5 MB (typically images), you can upload directly to the API in a single request using multipart form data. No presigned URL or confirmation step needed.
Form Fields
| Name | Type | Required | Description |
|---|---|---|---|
file | file | Required | The file to upload (max 5 MB) |
alt_text | string | Optional | Accessibility text for the media |
filefileRequiredThe file to upload (max 5 MB)
alt_textstringOptionalAccessibility text for the media
curl -X POST https://schedulala.com/api/v1/media \-H "Authorization: Bearer sk_live_abc123xyz789..." \-F "file=@hero-image.png" \-F "alt_text=Product hero image with blue gradient background"
{"success": true,"media": {"id": "media_def456","url": "https://storage.schedulala.com/media/media_def456.png","type": "image","mimeType": "image/png","size": 2458624,"altText": "Product hero image with blue gradient background","expiresAt": "2026-03-31T12:00:00.000Z"}}
Confirm presigned upload
/api/v1/media/:id/confirmConfirm that a presigned upload has completedThis endpoint is only needed for the presigned URL flow. Call it after uploading the file to the presigned URL to mark the media as ready. Media that is not confirmed within 1 hour will be automatically cleaned up.
curl -X POST https://schedulala.com/api/v1/media/media_abc123/confirm \-H "Authorization: Bearer sk_live_abc123xyz789..."
{"success": true,"media": {"id": "media_abc123","url": "https://storage.schedulala.com/media/media_abc123.mp4","status": "ready","type": "video","mimeType": "video/mp4","size": 52428800,"altText": "Product demo walkthrough","expiresAt": "2026-03-31T12:00:00.000Z"}}
Upload from a URL
/api/v1/media/from-urlDownload a public image or video and re-host it on the Schedulala CDNIf your media already lives at a public URL (a web image, a temporary or expiring link, an AI-generated image URL), you can skip uploading bytes yourself. Pass the URL and the server downloads the file (30 second timeout, up to 3 redirects), identifies the real file type from the file bytes (the origin's Content-Type header is ignored), validates its size, and re-hosts it on the Schedulala CDN. You get back a stable URL plus a media id, ready to use in post creation. No confirmation step is needed.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
url | string | Required | Public http(s) URL of the image or video to download |
type | string | Optional | Optional hint, "image" or "video". Verified against the actual file bytes; a mismatch is rejected |
altText | string | Optional | Accessibility text for the media (max 1000 characters) |
fileName | string | Optional | File name for the re-hosted media. Defaults to the URL basename |
urlstringRequiredPublic http(s) URL of the image or video to download
typestringOptionalOptional hint, "image" or "video". Verified against the actual file bytes; a mismatch is rejected
altTextstringOptionalAccessibility text for the media (max 1000 characters)
fileNamestringOptionalFile name for the re-hosted media. Defaults to the URL basename
Size and format limits
| Type | Formats | Max size |
|---|---|---|
| Image | jpeg, png, webp | 20 MB |
| GIF | gif | 15 MB |
| Video | mp4, mov, webm | 50 MB |
For videos larger than 50 MB, skip this endpoint and pass the public URL directly in the mediaItems array when creating a post; the media is fetched at publish time. Re-hosted media expires after 30 days if it is not attached to a post.
Security: The URL must be publicly reachable without login. Private and internal addresses are rejected with the error code url_not_allowed. Every redirect hop is checked against the same rules.
curl -X POST https://schedulala.com/api/v1/media/from-url \-H "Authorization: Bearer sk_live_abc123xyz789..." \-H "Content-Type: application/json" \-d '{"url": "https://example.com/images/sunset.jpg","type": "image","altText": "Sunset over the marina"}'
{"success": true,"media": {"id": "media_ghi789","url": "https://storage.schedulala.com/media/media_ghi789.jpg","status": "ready","type": "image","mimeType": "image/jpeg","size": 1843200,"width": 1920,"height": 1080,"altText": "Sunset over the marina","sourceUrl": "https://example.com/images/sunset.jpg","expiresAt": "2026-07-11T12:00:00.000Z","createdAt": "2026-06-11T12:00:00.000Z"}}
width and height are included for images. sourceUrl echoes the URL you submitted.
Errors
| Code | Status | Meaning |
|---|---|---|
url_not_allowed | 400 | Not a public http(s) URL, or it resolves to a private or internal address |
download_failed | 422 | The source could not be downloaded (timeout, connection error, non-200 response, too many redirects, empty file) |
media_too_large | 413 | The file exceeds the size limit for its type |
unsupported_media | 415 | The file bytes are not a supported image (jpeg, png, webp, gif) or video (mp4, mov, webm) |
A validation_error (400) is returned when the type hint does not match what the file actually contains.
Upload from AI chats (inline base64)
/api/v1/mediaJSON body with base64 data (small images, for chat clients)AI chat clients (the Claude or ChatGPT connector) cannot hand the API a local file, so small images can travel inline: send a JSON body with base64 data and its mimeType. Images only, 3 MB max decoded. The real file type is verified from the bytes.
{"data": "<base64-encoded image>","mimeType": "image/png","fileName": "product.png","altText": "Product shot on white background"}
Videos and larger files cannot go inline. Upload them once in the dashboard Media library (up to 500 MB, straight from the browser to storage), or use the presigned flow above, then reference them with the list endpoint below. This is how AI assistants post videos: the file never travels through the conversation, only its URL does.
Chunked upload (AI-generated images)
Some AI clients truncate large tool-call arguments, so a base64 image a model generated itself (a chart, quote card, or edited photo) can be too big to send in one inline request. The chunked flow splits the base64 across many small calls. It is images only, with a 3 MB decoded ceiling, and has three steps.
/api/v1/media/uploadsBegin a chunked upload session (images only)/api/v1/media/uploads/:id/chunksAppend one base64 chunk (8000 chars max, in order)/api/v1/media/uploads/:id/completeAssemble, validate, and host the image/api/v1/media/uploads/:idAbort and discard the sessionStep 1: Begin the session
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
mimeType | string | Required | Image MIME type (e.g. "image/png"). Videos are not supported for chunked upload |
fileName | string | Optional | File name to store under |
altText | string | Optional | Accessibility text for the image |
mimeTypestringRequiredImage MIME type (e.g. "image/png"). Videos are not supported for chunked upload
fileNamestringOptionalFile name to store under
altTextstringOptionalAccessibility text for the image
{"mimeType": "image/png","fileName": "revenue-chart.png","altText": "Weekly revenue chart"}
{"success": true,"upload": {"id": "665f1c2b9a3e4d0012ab34cd","chunkSizeChars": 8000,"maxBytes": 3145728,"expiresAt": "2026-07-13T12:30:00.000Z"}}
Step 2: Send the base64 in order
Split the image's base64 into pieces of at most 8000 characters and POST them one at a time, with seq starting at 0 and incrementing by 1. Each response returns the next expected sequence.
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
seq | number | Required | 0-based chunk index. Must match the next expected sequence |
data | string | Required | Base64 chunk, 8000 characters max |
seqnumberRequired0-based chunk index. Must match the next expected sequence
datastringRequiredBase64 chunk, 8000 characters max
{ "seq": 0, "data": "iVBORw0KGgoAAAANSUhEUgAA...." }
{"success": true,"received": 1,"nextSeq": 1}
If a chunk arrives out of order you get a 409 seq_mismatch whose details.expectedSeq tells you exactly where to resume, so retries are deterministic. A chunk over 9000 characters is rejected with chunk_too_large (keep chunks at 8000). Sessions expire 30 minutes after they begin, returning 410 upload_expired.
Step 3: Complete the upload
Once every chunk is sent, complete the session. The server joins the chunks, decodes the base64, verifies the bytes are a real image (jpeg, png, gif, webp), and hosts it on the Schedulala CDN. The response matches the other upload endpoints, so the media id and url drop straight into a post.
curl -X POST https://schedulala.com/api/v1/media/uploads/665f1c2b9a3e4d0012ab34cd/complete \-H "Authorization: Bearer sk_live_abc123xyz789..."
{"success": true,"media": {"id": "media_jkl012","url": "https://storage.schedulala.com/media/media_jkl012.png","type": "image","mimeType": "image/png","size": 184320,"width": 1200,"height": 675,"altText": "Weekly revenue chart","expiresAt": "2026-08-12T12:00:00.000Z"}}
To discard a session you started but no longer need, send DELETE /api/v1/media/uploads/:id. The MCP connector exposes this whole flow as the begin_generated_media_upload, append_generated_media_chunk, and finish_generated_media_upload tools, for images a model generates itself. Files on the user's device still go through the in-chat uploader (attach_media), which preserves full quality.
List media
/api/v1/mediaRecent confirmed uploads for your account, newest firstReturns your recent uploads across the dashboard Media library and every API key: id, url, type (image or video), size and file name, ready to drop into mediaItems. Query params: type=image|video, limit (max 50, default 20). The MCP connector exposes this as the list_media tool, so users can say "post my latest upload" in Claude or ChatGPT.
Tip: prefer uploading through these media endpoints over hot-linking external URLs in mediaItems. Uploaded media is validated against each platform's size and format limits at post creation, so violations fail with a clear 400 instead of at publish time.
Using media in posts
After uploading, use the media id or url in the mediaItems array when creating a post:
{"content": "Check out our new product demo!","platforms": [{ "platform": "twitter", "accountId": "acc_tw_123" }],"mediaItems": [{"mediaId": "media_abc123","altText": "Product demo walkthrough"}],"publishNow": true}
Media notes
| Limit | Value |
|---|---|
| Presigned upload — max video size | 1 GB |
| Presigned upload — max image size | 20 MB |
| Direct multipart upload — max size | 5 MB |
| From-URL upload, max size | 20 MB image / 15 MB GIF / 50 MB video |
| Presigned URL expiry | 1 hour |
| Unused media expiry | 30 days |
Supported formats
Video
mp4movwebmImage
jpgjpegpnggifwebpTip: Uploaded media expires after 30 days if not attached to a post. Presigned URLs expire after 1 hour — if the upload doesn't complete in time, request a new URL.
Platform limits: Each social platform has its own media requirements (aspect ratios, file sizes, durations). Schedulala validates media against the target platform's limits when you create the post, not at upload time. Check the Posts API docs for platform-specific constraints.
Per-platform media constraints
Each social platform has unique media requirements. Schedulala validates media against the target platform's limits when you create the post. This table summarizes the key constraints.
| Platform | Max image | Max video | Max items | Image formats | Video formats | Aspect ratio | Min dimensions | Max duration | GIF |
|---|---|---|---|---|---|---|---|---|---|
| 5 MB | 512 MB | 4 | JPEG, PNG, GIF, WebP | MP4, MOV | 1:3 — 3:1 | 4×4 | 2m 20s | ✓ | |
| 100 MB | 500 MB | 10 | JPEG, PNG, WebP | MP4, MOV | 4:5 — 1.91:1 | 320×320 | 90s | — | |
| 4 MB | 4 GB | 10 | JPEG, PNG, GIF, WebP | MP4, MOV | Any | 100×100 | 4h | ✓ | |
| Threads | 8 MB | 1 GB | 10 | JPEG, PNG, WebP | MP4, MOV | 4:5 — 1.91:1 | 320×320 | 5m | — |
| 10 MB | 5 GB | 1 | JPEG, PNG, GIF | MP4 | 1:2.4 — 2.4:1 | 200×200 | 10m | ✓ | |
| TikTok | 10 MB | 500 MB | 35 | JPEG, WebP | MP4, WebM | 9:16 — 1:1 | 360×360 | 10m | — |
| Bluesky | 1 MB | 100 MB | 4 | JPEG, PNG, WebP | MP4 | Any | Any | 60s | ✓ |
| YouTube | — | 2 GB | 1 | — | MP4, MOV, AVI, WebM | Any | 426×240 | No limit | — |
| 32 MB | 2 GB | 20 | JPEG, PNG, WebP | MP4 | 1:2.1 — 2:3 | 100×100 | 15m | — | |
| Telegram | 10 MB | 50 MB | 10 | JPEG, PNG | MP4 | Any | Any | No limit | ✓ |
Media best practices
Follow these guidelines to maximize compatibility when posting to multiple platforms simultaneously.
Use JPEG/PNG for images, MP4 for video
JPEG and PNG images are accepted by all 12 platforms. MP4 (H.264) video is supported everywhere except Google Business Profile (image-only). Avoid WebP images if targeting TikTok (JPEG/WebP only) or Telegram (JPEG/PNG only). Avoid MOV video if targeting LinkedIn, Bluesky, Pinterest, or Telegram.
Use 1:1 (square) as the safe default aspect ratio
A 1:1 square image falls within every platform's allowed range. If you need to optimize for a specific platform, use 4:5 for Instagram/Threads (portrait feed), 16:9 for YouTube/Twitter (landscape), or 2:3 for Pinterest (tall pin).
Keep images under 1 MB for maximum compatibility
Bluesky has a strict 1 MB image size limit — the tightest of any platform. If you're cross-posting to Bluesky, compress images before upload. Facebook's 4 MB limit is the next threshold to watch.
Video encoding tips
Use H.264 codec with AAC audio in an MP4 container for maximum compatibility. Keep video under 50 MB if targeting Telegram (strictest video limit). For TikTok and Instagram, target vertical (9:16) orientation. For YouTube and Twitter, target 16:9 landscape.
GIF compatibility
GIFs are supported by Twitter, Facebook, LinkedIn, Bluesky, and Telegram. They are not supported by Instagram, TikTok, Threads, YouTube, or Pinterest. For maximum reach, convert GIFs to short MP4 videos instead.
Pinterest requires tall images
Pinterest's aspect ratio range (1:2.1 to 2:3) strongly favors portrait/tall images. Square or landscape images will work but perform poorly in the feed. If Pinterest is a target, prepare a separate tall image.
Tip: For a complete reference of every platform's constraints, including text limits and post types, see the Platforms page.