Overview
Media
Upload images, videos and documents and use them in your posts.
Upload a file and attach it to your post, or use a public HTTPS URL that meets the Media URL Requirements.
Upload a File
Create the Media Item
Send the file name, its type and its exact size in bytes. The type is one of the Supported Formats, and the size has to be within its limit.
import { Configuration, MediaApi } from "@onepostly/sdk";
const mediaApi = new MediaApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await mediaApi.createMedia({
createMediaBody: {
"filename": "launch.mp4",
"contentType": "video/mp4",
"size": 48203776
},
});
console.log(response);{
"id": "med_7f3a9c2e1b4d4e8f9a6b5c4d3e2f1a0b",
"url": "https://cdn.onepostly.com/media/5b0e7c1a-2f4d-4c8e-9a3b-6d1f0e2c4a7b/7f3a9c2e-1b4d-4e8f-9a6b-5c4d3e2f1a0b.mp4",
"contentType": "video/mp4",
"sizeBytes": null,
"filename": "launch.mp4",
"status": "pending",
"createdAt": "2027-03-15T09:00:00.000Z",
"uploadUrl": "https://5d1e8a3c9f2b4e7a.r2.cloudflarestorage.com/onepostly-media/temp/5b0e7c1a-2f4d-4c8e-9a3b-6d1f0e2c4a7b/7f3a9c2e-1b4d-4e8f-9a6b-5c4d3e2f1a0b.mp4?X-Amz-Expires=3600&X-Amz-Date=20270315T090000Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=EXAMPLEKEY%2F20270315%2Fauto%2Fs3%2Faws4_request&X-Amz-SignedHeaders=content-length%3Bcontent-type%3Bhost&X-Amz-Signature=0f4c2b9e7a1d3f5b8c6e0a2d4f6b8e1c3a5d7f9b2e4c6a8d0f1b3e5c7a9d2f4b",
"uploadExpiresIn": 3600
}The response is 201 with the new media item.
| Field | Description |
|---|---|
id | The id you use in a post |
url | The file's public URL. It never changes, and serves the file once status is ready |
status | pending until the file arrives and is checked, then ready |
sizeBytes | The file size, null until status is ready |
uploadUrl | Where you send the file |
uploadExpiresIn | Seconds until uploadUrl stops working, 3600 |
Upload the File
Send the file with a PUT request to uploadUrl. You do not need an API key, because the URL itself allows the upload. The Content-Type header has to match the contentType you sent in step 1, and the file has to be exactly size bytes.
Send the whole file in one request. Your HTTP client sets Content-Length from the file, as in the examples below. Any other length is refused with 403, and nothing is stored. To upload a different file, create a new media item.
import { readFile } from "node:fs/promises";
await fetch(uploadUrl, {
method: "PUT",
headers: { "Content-Type": "video/mp4" },
body: await readFile("launch.mp4"),
});Each upload is checked against the type and size you declared, usually within seconds. To see the result, call GET /v1/media/{id}, which also checks the file if that has not happened yet. A checked file shows the status ready and its size in sizeBytes, and url serves it from then on.
Use It in a Post
Put the id in media. See Create Posts for the full request.
import { Configuration, PostsApi } from "@onepostly/sdk";
const postsApi = new PostsApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await postsApi.createPost({
createPostBody: {
"text": "Launch day",
"media": [
{
"id": "med_7f3a9c2e1b4d4e8f9a6b5c4d3e2f1a0b"
}
],
"accounts": [
{
"accountId": "acc_YOUR_X_ACCOUNT_ID"
}
]
},
});
console.log(response);{
"groupId": "grp_7d2e5b1a9c4f4a68b3e07f1c2d9a6e45",
"data": [
{
"id": "pst_3c8a2d6e91b44f7a8e250d6b4c1f9a73",
"groupId": "grp_7d2e5b1a9c4f4a68b3e07f1c2d9a6e45",
"accountId": "acc_YOUR_X_ACCOUNT_ID",
"platform": "x",
"source": "onepostly",
"status": "publishing",
"text": "Launch day",
"mediaKind": "video",
"media": [
{
"url": "https://cdn.onepostly.com/media/5b0e7c1a-2f4d-4c8e-9a3b-6d1f0e2c4a7b/7f3a9c2e-1b4d-4e8f-9a6b-5c4d3e2f1a0b.mp4"
}
],
"thread": [],
"scheduledFor": null,
"timezone": null,
"options": {},
"platformPostId": null,
"url": null,
"error": null,
"publishedAt": null,
"metrics": null,
"createdAt": "2027-03-15T10:00:00.000Z",
"updatedAt": "2027-03-15T10:00:00.000Z"
}
]
}In media and thread[].media, each item takes an id or a url, not both. The post shows every file by its url, even one you sent by id.
The files set the post kind. One video makes a video post, and several files make a multi_image post. For a Facebook reel and for stories, set mediaKind yourself. See Media Kinds for the full list.
Expiry
| File | Kept for |
|---|---|
uploadUrl | 1 hour |
| A file no post uses | 7 days |
| A file in a scheduled post | Until the post publishes |
| A file in a published post | No expiry |
An expired uploadUrl needs a new media item.
Media URL Requirements
Every media URL must meet these rules, uploaded or self-hosted.
- HTTPS to a public address.
- At most 2,048 characters.
- Returns the file with
200,Content-Lengthand the rightContent-Type. - At most 5 redirects, each to a public HTTPS address.
- No stall longer than 30 seconds.
Share links from Google Drive, Dropbox, OneDrive, SharePoint and iCloud fail. They return a web page, not the file. Open the URL in a private window to check it. If you see a page, upload the file to Onepostly.
The first two rules are checked when you create the post and return 400 VALIDATION_ERROR. The rest are checked at publish and fail the post with MEDIA_UNFETCHABLE. Uploaded files meet every rule once their status is ready.
Supported Formats
Onepostly accepts these files, and each platform adds its own limits. See the media requirements on each platform page, such as Instagram.
| Format | contentType | Max size |
|---|---|---|
| JPEG | image/jpeg | 30 MB |
| PNG | image/png | 30 MB |
| GIF | image/gif | 30 MB |
| WebP | image/webp | 30 MB |
| MP4 | video/mp4 | 1 GB |
| MOV | video/quicktime | 1 GB |
| AVI | video/x-msvideo or video/avi | 1 GB |
| WebM | video/webm | 1 GB |
application/pdf | 100 MB | |
| PPT | application/vnd.ms-powerpoint | 100 MB |
| PPTX | application/vnd.openxmlformats-officedocument.presentationml.presentation | 100 MB |
| DOC | application/msword | 100 MB |
| DOCX | application/vnd.openxmlformats-officedocument.wordprocessingml.document | 100 MB |
A larger size is refused with 400 MEDIA_TOO_LARGE before the upload starts.
Platform Limits
Each platform takes only some formats and sizes. They are in the Media Requirements on its page, such as Instagram.
- An uploaded file outside them returns
400when you create or edit the post, and nothing is created.paramnames the file, such asmedia.0orthread.1.media.0. - A file sent by
urlis checked when the post publishes, and the post fails with the same code.
| Code | Cause |
|---|---|
MEDIA_FORMAT_UNSUPPORTED | The platform does not take the file's format for this kind of post |
MEDIA_TOO_LARGE_FOR_PLATFORM | The file is over the platform's size limit for this kind of post |
The Content-Type sets a file's format. When it is missing or application/octet-stream, the URL's extension sets it.
Media per Platform
The mediaKind values and the file count each platform takes.
| Platform | text | image | multi_image | video | reel | document | stories | Max files |
|---|---|---|---|---|---|---|---|---|
| X (Twitter) | No | No | No | 4 | ||||
| No | As a reel | No | 10 | |||||
| No | 10 | |||||||
| Threads | No | No | No | 20 | ||||
| No | No | 20 | ||||||
| YouTube | No | No | No | No | No | No | 1 | |
| TikTok | No | No | No | No | 35 | |||
| No | No | No | No | No | 1 | |||
| Bluesky | No | No | No | 4 |
Durations, sizes and dimensions are on each platform page, such as X. See Compare Platforms to view all platforms side by side.
List Uploads
GET /v1/media lists the files you uploaded, newest first. Media items still waiting for their upload are not listed.
import { Configuration, MediaApi } from "@onepostly/sdk";
const mediaApi = new MediaApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await mediaApi.listMedia({
limit: 20,
});
console.log(response);{
"data": [
{
"id": "med_7f3a9c2e1b4d4e8f9a6b5c4d3e2f1a0b",
"url": "https://cdn.onepostly.com/media/5b0e7c1a-2f4d-4c8e-9a3b-6d1f0e2c4a7b/7f3a9c2e-1b4d-4e8f-9a6b-5c4d3e2f1a0b.mp4",
"contentType": "video/mp4",
"sizeBytes": 48203776,
"filename": "launch.mp4",
"status": "ready",
"createdAt": "2027-03-15T09:00:00.000Z"
},
{
"id": "med_7f3a9c2e1b4d4e8f9a6b5c4d3e2f1a02",
"url": "https://cdn.onepostly.com/media/5b0e7c1a-2f4d-4c8e-9a3b-6d1f0e2c4a7b/7f3a9c2e-1b4d-4e8f-9a6b-5c4d3e2f1a02.jpg",
"contentType": "image/jpeg",
"sizeBytes": 482113,
"filename": "spring.jpg",
"status": "ready",
"createdAt": "2027-03-14T16:30:00.000Z"
}
],
"nextCursor": null,
"hasMore": false
}You get 50 files per page by default and up to 100 with limit. Pass nextCursor as cursor to get the next page, until hasMore is false. Add include=total to get the total count. See Pagination.
Delete an Upload
import { Configuration, MediaApi } from "@onepostly/sdk";
const mediaApi = new MediaApi(
new Configuration({ accessToken: process.env.ONEPOSTLY_API_KEY })
);
const response = await mediaApi.deleteMedia({
id: "med_7f3a9c2e1b4d4e8f9a6b5c4d3e2f1a0b",
});
console.log(response);This returns 204 and deletes the file for good. Published posts are not affected, because each platform keeps its own copy.
A scheduled post that still uses the file fails when it publishes, so delete files only once all their posts are live.
Errors
These come back from a media request or POST /v1/posts.
| Code | HTTP | Cause | Fix |
|---|---|---|---|
VALIDATION_ERROR | 400 | A field is missing or too long, a media item has both or neither of id and url, or a URL breaks the rules | Fix the field in param |
UNSUPPORTED_MEDIA_TYPE | 400 | contentType is not supported | Convert the file |
MEDIA_TOO_LARGE | 400 | size is over the limit for contentType | Compress the file |
MEDIA_FORMAT_UNSUPPORTED | 400 | An account's platform does not take the uploaded file's format | Convert the file. See Platform Limits |
MEDIA_TOO_LARGE_FOR_PLATFORM | 400 | The uploaded file is over an account's platform limit | Compress the file. See Platform Limits |
NOT_FOUND | 404 | No media item has this id | Check the id with GET /v1/media |
MEDIA_NOT_FOUND | 404 | A post's media[].id does not exist | Use an id from GET /v1/media |
SERVICE_UNAVAILABLE | 503 | Uploads are down for now | Try again later |
File problems show up later, in the post's error.code. See Failed Posts.
| Code | Cause | Fix |
|---|---|---|
MEDIA_NOT_UPLOADED | The file was never uploaded, or the upload expired | Upload the file, then create the post again |
MEDIA_TYPE_MISMATCH | The file's Content-Type is not its contentType | Create a new media item with the right type |
MEDIA_UNFETCHABLE | The URL breaks a URL rule | Fix the URL |
MEDIA_FORMAT_UNSUPPORTED | The platform does not take the URL's file format | Convert the file. See Platform Limits |
MEDIA_TOO_LARGE_FOR_PLATFORM | The URL's file is over the platform's limit | Compress the file. See Platform Limits |
INVALID_MEDIA | The file is empty, or the platform rejects it | Check the platform's Media Requirements |
URL_OWNERSHIP_UNVERIFIED | TikTok refuses the URL's domain | Upload the file and use its id |
Other codes are in Errors.