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 URL | Option A — pass it into the post, or import it |
| A file on the machine making the request | Option B — POST /v0/media/upload |
| A file on someone else's device | An upload link — see Manage media |
| A large file, or bytes to stream to storage | Option 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.
The optional intent field picks the storage target and size limit:
intent | Use for | Max size | Storage |
|---|---|---|---|
post (default) | Media you'll attach to posts | 250 MB | Public, served from r2.schedulin.app |
public-asset | Avatars, logos, and other public assets | 250 MB | Public, 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" }'