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

POST/api/v1/media/upload-urlGet presigned upload URL (large files)
POST/api/v1/mediaSimple multipart upload (small files)
POST/api/v1/media/from-urlRe-host a public URL on the Schedulala CDN
GET/api/v1/mediaList your recent uploads (Media library + API)
POST/api/v1/media/:id/confirmConfirm presigned upload completed

Get presigned upload URL

POST/api/v1/media/upload-urlRequest a presigned URL for uploading large files directly to storage

Use this endpoint for files larger than 5 MB (up to 1 GB for video, 20 MB for images). The upload flow has three steps:

1

Request a presigned URL

POST to /api/v1/media/upload-url with file metadata. You'll receive an upload URL, method, and headers.

2

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.

3

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

fileName
stringRequired

Original file name (e.g., "product-demo.mp4")

mimeType
stringRequired

MIME type of the file (e.g., "video/mp4", "image/png")

sizeBytes
numberRequired

File size in bytes

altText
stringOptional

Accessibility 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

POST/api/v1/mediaUpload small files directly via multipart form data

For 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

file
fileRequired

The file to upload (max 5 MB)

alt_text
stringOptional

Accessibility 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

POST/api/v1/media/:id/confirmConfirm that a presigned upload has completed

This 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

POST/api/v1/media/from-urlDownload a public image or video and re-host it on the Schedulala CDN

If 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

url
stringRequired

Public http(s) URL of the image or video to download

type
stringOptional

Optional hint, "image" or "video". Verified against the actual file bytes; a mismatch is rejected

altText
stringOptional

Accessibility text for the media (max 1000 characters)

fileName
stringOptional

File name for the re-hosted media. Defaults to the URL basename

Size and format limits

TypeFormatsMax size
Imagejpeg, png, webp20 MB
GIFgif15 MB
Videomp4, mov, webm50 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

CodeStatusMeaning
url_not_allowed400Not a public http(s) URL, or it resolves to a private or internal address
download_failed422The source could not be downloaded (timeout, connection error, non-200 response, too many redirects, empty file)
media_too_large413The file exceeds the size limit for its type
unsupported_media415The 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)

POST/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.

POST /api/v1/media (application/json)
{
"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.

POST/api/v1/media/uploadsBegin a chunked upload session (images only)
POST/api/v1/media/uploads/:id/chunksAppend one base64 chunk (8000 chars max, in order)
POST/api/v1/media/uploads/:id/completeAssemble, validate, and host the image
DELETE/api/v1/media/uploads/:idAbort and discard the session

Step 1: Begin the session

Request Body

mimeType
stringRequired

Image MIME type (e.g. "image/png"). Videos are not supported for chunked upload

fileName
stringOptional

File name to store under

altText
stringOptional

Accessibility text for the image

POST /api/v1/media/uploads
{
"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

seq
numberRequired

0-based chunk index. Must match the next expected sequence

data
stringRequired

Base64 chunk, 8000 characters max

POST /api/v1/media/uploads/:id/chunks
{ "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

GET/api/v1/mediaRecent confirmed uploads for your account, newest first

Returns 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:

Post with uploaded media
{
"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

LimitValue
Presigned upload — max video size1 GB
Presigned upload — max image size20 MB
Direct multipart upload — max size5 MB
From-URL upload, max size20 MB image / 15 MB GIF / 50 MB video
Presigned URL expiry1 hour
Unused media expiry30 days

Supported formats

Video

mp4movwebm

Image

jpgjpegpnggifwebp

Tip: 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.

PlatformMax imageMax videoMax itemsImage formatsVideo formatsAspect ratioMin dimensionsMax durationGIF
Twitter5 MB512 MB4JPEG, PNG, GIF, WebPMP4, MOV1:3 — 3:14×42m 20s
Instagram100 MB500 MB10JPEG, PNG, WebPMP4, MOV4:5 — 1.91:1320×32090s
Facebook4 MB4 GB10JPEG, PNG, GIF, WebPMP4, MOVAny100×1004h
Threads8 MB1 GB10JPEG, PNG, WebPMP4, MOV4:5 — 1.91:1320×3205m
LinkedIn10 MB5 GB1JPEG, PNG, GIFMP41:2.4 — 2.4:1200×20010m
TikTok10 MB500 MB35JPEG, WebPMP4, WebM9:16 — 1:1360×36010m
Bluesky1 MB100 MB4JPEG, PNG, WebPMP4AnyAny60s
YouTube2 GB1MP4, MOV, AVI, WebMAny426×240No limit
Pinterest32 MB2 GB20JPEG, PNG, WebPMP41:2.1 — 2:3100×10015m
Telegram10 MB50 MB10JPEG, PNGMP4AnyAnyNo 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.

Related