logo
Schedulin Developers

For AI agents: a documentation index is available at /llms.txt. Every guide and example is also available as markdown by adding .md to its URL.

Upload media

Media is attached to posts by url. Pick the first option that fits:

You have…Use
A public image/video URLOption A — pass it into the post, or import it
A file on the machine making the requestOption B — POST /v0/media/upload
A file on someone else's deviceAn upload link — see Manage media
A large file, or bytes to stream to storageOption C — presign → PUT → register

Every option adds the file to your workspace's media library; all post media is served from https://r2.schedulin.app. Files can be up to 250 MB; SVG and other active content is rejected.

Option A — Import from a public URL

The simplest path: pass any publicly reachable URL in the post's media array and POST /v0/posts downloads it for you. To import it ahead of time (for example to reuse it across several posts), call POST /v0/media/from-url:

curl -X POST https://api.schedulin.app/v0/media/from-url \
  -H "x-api-key: $SCHEDULIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://www.schedulin.app/opengraph-image.png", "alt": "Launch day" }'
const media = await client.media.createFromUrl({
  url: "https://www.schedulin.app/opengraph-image.png",
  alt: "Launch day",
});
const mediaUrl = media.url;
media = client.media.create_from_url(
    url="https://www.schedulin.app/opengraph-image.png",
    alt="Launch day",
)
media_url = media.url

The response is the media record — use its url in your post:

{
  "id": "6f0c…",
  "url": "https://r2.schedulin.app/<your-org>/<id>-opengraph-image.png",
  "name": "opengraph-image.png",
  "mimeType": "image/png",
  "...": "..."
}

The source must be public (no auth) http(s).

Option B — Upload a file directly (one call)

Send the raw bytes as multipart/form-data to POST /v0/media/upload. Optional fields: name, alt, and contentType (otherwise the type is inferred from the file name and bytes). Keep request bodies under about 4 MB — larger request bodies are rejected before they reach the API. For bigger files, use Option C.

curl -X POST https://api.schedulin.app/v0/media/upload \
  -H "x-api-key: $SCHEDULIN_API_KEY" \
  -F "file=@launch.jpg" \
  -F "alt=Launch day"

The SDKs' media.upload sends the same multipart request. Give the file a name and type so the stored media gets the right content type:

import { readFile } from "node:fs/promises";

// From a path (filename and type set explicitly)…
const media = await client.media.upload({
  file: { path: "launch.jpg", contentType: "image/jpeg" },
  alt: "Launch day",
});

// …or from bytes you already have: a Blob/File with a `type`
const blob = new Blob([await readFile("launch.jpg")], { type: "image/jpeg" });
const fromBlob = await client.media.upload({
  file: new File([blob], "launch.jpg", { type: "image/jpeg" }),
});
# A (filename, file object or bytes, content type) tuple
with open("launch.jpg", "rb") as f:
    media = client.media.upload(
        file=("launch.jpg", f, "image/jpeg"),
        alt="Launch day",
    )
media_url = media.url

The response is the media record (id, url, name, mimeType, …), the same shape as Option A — typed as Media in the SDKs (media.url in TypeScript and Python).

From the CLI, schedulin media upload ./launch.jpg picks the right method for the file size and prints the media id and url.

Option C — Presigned upload (large files)

For direct-to-storage uploads, request a presigned PUT from POST /v0/media/presign, upload the bytes yourself, then register the object with POST /v0/media/register. This is the recommended flow for files too large for a single request body (POST /v0/media/upload runs behind a ~4.5 MB request limit) and for streaming bytes you don't want to buffer. For small files, options A and B are one call.

# 1. presign (contentType and key are required)
curl -X POST https://api.schedulin.app/v0/media/presign \
  -H "x-api-key: $SCHEDULIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contentType": "image/jpeg", "key": "launch.jpg", "size": 248173 }'
# → { "url": "https://<bucket>.r2.cloudflarestorage.com/...", "key": "<your-org>/launch.jpg", "method": "PUT" }

# 2. PUT the raw bytes to the returned url within 10 minutes. If you sent `size`,
#    the url only accepts a body of exactly that many bytes.
curl -X PUT "<url>" -H "Content-Type: image/jpeg" --data-binary @launch.jpg

# 3. register the uploaded object in your media library (in place — no copy)
curl -X POST https://api.schedulin.app/v0/media/register \
  -H "x-api-key: $SCHEDULIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "key": "<your-org>/launch.jpg", "name": "launch.jpg", "alt": "Launch day" }'
const media = await client.media.register({ key: presigned.key, name: "launch.jpg" });
media = client.media.register(key=presigned.key, name="launch.jpg")

POST /v0/media/register returns the same media record as options A and B, pointing at the object you uploaded: its url is https://r2.schedulin.app/<key>, and deleting the media (DELETE /v0/media/{id}) removes that object from storage. The type and size are read from the stored object and must pass the same rules as any upload (image/video/audio, at most 250 MB, no SVG). Registering the same key again returns the existing record, so retries are safe. It returns 404 if no object was uploaded for the key in your workspace — for example when the PUT failed or the key belongs to another workspace.

NOTE
Without step 3 the bytes are stored but there's no media-library record: the file won't appear in and can't be deleted with . Its URL still works in (the first post that uses it registers it in place), but register it explicitly so you can manage it. Don't pass the uploaded file's URL to : that downloads a and leaves the presigned original behind. rejects a key whose bytes were never uploaded.

The optional intent field picks the storage target and size limit:

intentUse forMax sizeStorage
post (default)Media you'll attach to posts250 MBPublic, served from r2.schedulin.app
public-assetAvatars, logos, and other public assets250 MBPublic, served from r2.schedulin.app

Leave intent unset (or post) for anything you plan to publish.

Attach it to a post

Whichever option you used, put the resulting url in the media array:

curl -X POST https://api.schedulin.app/v0/posts \
  -H "x-api-key: $SCHEDULIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "socialAccountId": "acc_123",
    "caption": "Behind the scenes 📸",
    "media": [{ "url": "https://r2.schedulin.app/<your-org>/<id>-launch.jpg" }],
    "action": "now"
  }'