Media
Media
Upload a file once, then attach it to any number of posts via media_ids.
The library also works as a searchable asset store: alt_text and tags are searched as well
as the filename, so a clip described as "cable crossover at the gym" is found by "gym" even
though its filename is PXL_20260806_113344.mp4. Write a real alt_text at upload time — an
item with no description is only findable by someone who already knows its filename.
Audio (mp3, wav, m4a, aac) is accepted as a production input — music beds, voice takes —
and can never be attached to a post.
The media object
| Field | Type | Description |
|---|---|---|
id |
integer | Unique id. Pass to a post's media_ids. |
original_name |
string | The uploaded filename. |
mime_type |
string | e.g. image/jpeg, video/mp4. |
size_bytes |
integer | File size. |
cdn_url |
string | Public URL of the file. |
width / height |
integer|null | Pixel dimensions for images. |
duration |
number|null | Seconds, for video and audio. null means it was never measured, not zero. |
sha256 |
string|null | Checksum of the stored bytes. Use it to verify a local copy without re-downloading. |
folder |
string|null | Optional organizational folder. |
tags |
string[] | Optional tags. |
alt_text |
string|null | Accessibility text. |
Upload media
POST /api/media
Send as multipart/form-data.
| Field | Type | Required | Notes |
|---|---|---|---|
file |
file | yes | jpg, jpeg, png, gif, webp, mp4, mov, webm, m4v, mp3, wav, m4a, aac, pdf. Max 90 MB — use the resumable flow above that. |
folder |
string | no | Organizational folder. |
tags |
string[] | no | Each tag ≤ 50 characters. |
alt_text |
string | no | ≤ 500 characters. |
curl -X POST https://schedulenchill.com/api/media \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-F "file=@./launch.png" \
-F "folder=product-launch" \
-F "tags[]=launch" \
-F "alt_text=Product launch screenshot"
Returns 201 Created with the media object. Use its id in a post's media_ids.
Building over MCP or from a server with a public file URL? Use the
upload_mediatool, which ingests media by URL instead of multipart upload.
Upload a video from your own computer
The single-request upload above is capped, and a large video in one request is fragile — one dropped connection and you start over. For anything big, open a resumable session and stream the file in chunks. Nothing is capped by request size, and an interrupted upload picks up where it stopped.
This is what your AI tool does when you say "upload ~/Videos/demo.mp4 and schedule it" —
the MCP upload_media tool with source="local" returns exactly the envelope below.
1. Open a session
POST /api/media/uploads
| Field | Type | Required | Notes |
|---|---|---|---|
filename |
string | yes | Original name, with extension. |
mime_type |
string | yes | e.g. video/mp4. |
size_bytes |
integer | yes | Exact size. Finalize rejects a mismatch. |
sha256 |
string | no | Hex digest. When given, finalize verifies against it. |
folder |
string | no | Carried onto the finished library item. |
tags |
string[] | no | Carried onto the finished library item. |
alt_text |
string | no | Carried onto the finished library item. Set it here — a large upload that lands undescribed almost never gets one added later. |
A free account may upload up to 400 MB per file; the absolute cap is 1 GB. Going over fails at session-open with a 422 that names the limit, so nothing is streamed for nothing. GET /api/capabilities reports your ceiling in limits.max_upload_bytes — read it rather than assuming.
curl -X POST https://schedulenchill.com/api/media/uploads \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"filename":"demo.mp4","mime_type":"video/mp4","size_bytes":11402,
"sha256":"86e39c5a..."}'
{
"upload_id": "779b36fd-6d13-4c42-b3e5-c731956b093c",
"chunk_size": 8388608,
"received_bytes": 0,
"size_bytes": 11402,
"expires_at": "2026-08-04T07:35:44+00:00"
}
2. Stream the chunks
PATCH each chunk_size-byte slice as a raw body — not multipart — with offset set
to the running byte count (0, chunk_size, 2 × chunk_size, …).
curl -X PATCH "https://schedulenchill.com/api/media/uploads/UPLOAD_ID?offset=0" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @./chunk-0
{ "received_bytes": 11402, "size_bytes": 11402, "completed": true }
Send chunks one at a time, in order. Parallel PATCHes corrupt the assembled file.
If a chunk fails, re-GET the session to read received_bytes and resume from there —
that number is the source of truth, not your own bookkeeping.
3. Finalize
curl -X POST https://schedulenchill.com/api/media/uploads/UPLOAD_ID/complete \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
Returns the media object. Its id goes in a post's media_ids.
{ "id": 12, "original_name": "demo.mp4", "mime_type": "video/mp4", "size_bytes": 11402 }
Notes
- Use a token with
*abilities (the default). A restricted token 401s on the chunkPATCHes. /mcpand/api/*share one auth guard, so the same bearer works for both.- Sessions expire —
expires_atis in the envelope. Abandoned ones are pruned. DELETE /api/media/uploads/{id}cancels a session you no longer want.
List media
GET /api/media
Paginated, 15 per page. Optional filters:
| Query param | Description |
|---|---|
search |
Match filename, alt text or tags. |
type |
image or video. |
folder |
Filter by folder. |
tags |
Filter by tag. |
per_page |
Rows per page, 1–200. Over 200 returns 422. |
page |
Page number. |
Retrieve media
GET /api/media/{id}
Update media
PUT /api/media/{id}
Update metadata only — folder, tags, alt_text. The file itself is immutable; upload a new file to replace it.
Delete media
DELETE /api/media/{id}
Returns 204 No Content.