Publishing
Upload images, videos and documents with a presigned URL, attach them to posts with mediaItems, and add alt text, thumbnails and captions.
Posts carry media as URLs in mediaItems. If your file is already on a public HTTPS URL, use that URL directly. If it lives on your server or the user's device, upload it to Pozy first: you ask for an upload URL, send the file there, and put the returned public URL in the post. One upload can be reused by every platform in a post and by later posts.
#Upload a file
#Ask for an upload URL
POST /v1/media/presign takes the file name and its MIME type. Add size in bytes to check the 5 GB limit before you upload.
curl -X POST https://api.pozy.ai/v1/media/presign \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "filename": "demo.mp4", "contentType": "video/mp4", "size": 48213504 }'const uploadRes = await fetch("https://api.pozy.ai/v1/media/presign", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ filename: "demo.mp4", contentType: "video/mp4", size: 48213504 }),
});
const upload = await uploadRes.json();
if (!uploadRes.ok) throw new Error(upload.error);upload = requests.post(
"https://api.pozy.ai/v1/media/presign",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={"filename": "demo.mp4", "contentType": "video/mp4", "size": 48213504},
timeout=30,
).json(){
"uploadUrl": "https://…signed-upload-url…",
"publicUrl": "https://pozy.ai/media/temp/1730289600_r7t2_demo.mp4",
"key": "temp/1730289600_r7t2_demo.mp4",
"expiresIn": 3600
}uploadUrl is valid for expiresIn seconds (one hour). publicUrl is the address you put in the post.
#PUT the file
Send the raw bytes to uploadUrl with the same Content-Type you declared. Do not add an Authorization header: the signature in the URL is the permission.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: video/mp4" \
--data-binary @demo.mp4import { readFile } from "node:fs/promises";
await fetch(upload.uploadUrl, {
method: "PUT",
headers: { "Content-Type": "video/mp4" },
body: await readFile("demo.mp4"),
});with open("demo.mp4", "rb") as f:
requests.put(upload["uploadUrl"], data=f, headers={"Content-Type": "video/mp4"}, timeout=600).raise_for_status()A successful upload answers 200 with an empty body.
#Attach it to a post
Put publicUrl in mediaItems:
curl -X POST https://api.pozy.ai/v1/posts \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Two minutes, start to finish.",
"mediaItems": [{ "type": "video", "url": "https://pozy.ai/media/temp/1730289600_r7t2_demo.mp4" }],
"platforms": [
{ "platform": "youtube", "accountId": "6710c3e2a9f14b0d8e5c7a27", "platformSpecificData": { "title": "Pozy in two minutes" } },
{ "platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21" }
],
"queuedFromProfile": "670f9b1d4c2e8a7f3b6d5e10"
}'const res = await fetch("https://api.pozy.ai/v1/posts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "Two minutes, start to finish.",
mediaItems: [{ type: "video", url: upload.publicUrl }],
platforms: [
{ platform: "youtube", accountId: "6710c3e2a9f14b0d8e5c7a27", platformSpecificData: { title: "Pozy in two minutes" } },
{ platform: "linkedin", accountId: "6710c3e2a9f14b0d8e5c7a21" },
],
queuedFromProfile: "670f9b1d4c2e8a7f3b6d5e10",
}),
});
if (!res.ok) throw new Error((await res.json()).error);requests.post(
"https://api.pozy.ai/v1/posts",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={
"content": "Two minutes, start to finish.",
"mediaItems": [{"type": "video", "url": upload["publicUrl"]}],
"platforms": [
{"platform": "youtube", "accountId": "6710c3e2a9f14b0d8e5c7a27", "platformSpecificData": {"title": "Pozy in two minutes"}},
{"platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21"},
],
"queuedFromProfile": "670f9b1d4c2e8a7f3b6d5e10",
},
timeout=30,
).raise_for_status()#Uploads through the API: about 4 MB
A request to the API itself can carry about 4 MB. Endpoints that take the file in the request body are limited by that: POST /v1/media/upload-direct, porting and identity documents, SMS opt-in screenshots, RCS logos and banners, the WhatsApp profile photo and CSV bulk uploads. A larger body is refused with 413 before Pozy reads it, without the usual JSON error.
Use upload-direct for files up to 4 MB. For anything larger, use POST /v1/media/presign and send the file straight to storage as shown above: presigned uploads take files up to 5 GB.
#Keep uploads alive
Uploaded files sit in temporary storage for 7 days. When a post that uses the file is created or updated, Pozy copies it to permanent storage, so a post scheduled months ahead keeps its media.
The catch is the gap between upload and post. If your app uploads when the person picks a file but creates the post a week later, the file is gone. A draft still saves, but scheduling or publishing it fails with 400. Either create the post soon after the upload, or upload when the post is created.
#Accepted file types
| Kind | Formats | contentType values |
|---|---|---|
| Images | JPEG, PNG, WebP, GIF | image/jpeg, image/png, image/webp, image/gif |
| Videos | MP4, MPEG, MOV, AVI, WebM, M4V | video/mp4, video/mpeg, video/quicktime, video/avi, video/x-msvideo, video/webm, video/x-m4v |
| Documents | PDF (LinkedIn document posts) | application/pdf |
| Audio | MP3, M4A, AAC, OGG, WAV, WebM | audio/mpeg, audio/mp4, audio/x-m4a, audio/aac, audio/ogg, audio/wav, audio/webm |
| Captions | SRT, WebVTT | application/x-subrip, text/vtt |
Every type can be up to 5 GB at upload. Platforms are much stricter, and each has its own rules for count, size, duration and aspect ratio: see the media section of each platform page. Audio cannot be posted on its own. Use it as an attachment in inbox messages.
A presign request with a type outside this list, such as application/octet-stream, returns 400 with code: "invalid_field_value" and param: "contentType".
#Media item fields
| Field | What it does |
|---|---|
type |
image, video, gif or document. Pozy infers it from the file extension when you leave it out, and rejects a type that contradicts the extension. |
url |
Public HTTPS URL that returns the file itself. |
altText |
Accessibility text for images. Used where the platform supports it, for example Instagram (cut at 1,000 characters) and Bluesky. |
thumbnail |
Custom cover for a video, JPG or PNG up to 10 MB. Used for Facebook videos and Reels, YouTube and LinkedIn videos. |
instagramThumbnail |
Cover image for an Instagram Reel. |
subtitles |
Up to 20 caption files for a video, each { "url": "...", "language": "en" }. |
title, filename, size, mimeType |
Optional details stored with the item. |
Order matters: items are published in the order you list them, which sets the order of a carousel.
#Captions on videos
Attach SRT or WebVTT files, one per language, and Pozy converts them for each platform that accepts captions:
[
{
"type": "video",
"url": "https://pozy.ai/media/temp/1730289600_r7t2_demo.mp4",
"subtitles": [
{ "url": "https://pozy.ai/media/temp/1730289600_c1a9_demo.en.vtt", "language": "en" },
{ "url": "https://pozy.ai/media/temp/1730289600_c1b3_demo.es.srt", "language": "es" }
]
}
]Caption files must be public and under 1 MB. A platform that cannot take a track skips it and adds a warning to the response. A file Pozy cannot parse returns 400.
#Different media per platform
mediaItems applies to every entry. Use customMedia on one platforms[] entry to replace it there, for example a square crop for Instagram and a wide one elsewhere:
{
"mediaItems": [{ "type": "image", "url": "https://pozy.ai/media/temp/1730289600_k3f9_wide.jpg" }],
"platforms": [
{ "platform": "linkedin", "accountId": "6710c3e2a9f14b0d8e5c7a21" },
{
"platform": "instagram",
"accountId": "6710c3e2a9f14b0d8e5c7a22",
"customMedia": [{ "type": "image", "url": "https://pozy.ai/media/temp/1730289600_m2p7_square.jpg" }]
}
]
}#Prepare a file for a platform
When you are not sure a file meets a platform's rules, POST /v1/media/prepare runs the same conversion Pozy applies before publishing and gives you the result. Send the file URL and a target:
{ "url": "https://pozy.ai/media/temp/1730289600_r7t2_demo.mp4", "target": "instagram" }target is one of instagram, instagram_story, facebook, facebook_reel, facebook_story, threads, tiktok, linkedin, twitter, bluesky, pinterest, reddit, snapchat, telegram, googlebusiness or youtube_thumbnail. The response says whether anything changed, and gives the new url with originalSize and outputSize. Images are converted to a format the platform accepts and compressed under its limit; videos are re-encoded or resized when needed.
The call is synchronous, so a long video can take a few minutes. A workspace can prepare up to 60 files per hour; past that you get 429.
#URLs that fail
The most common media failure is a URL that does not return the file. Links to Google Drive, Dropbox, OneDrive or iCloud pages return an HTML page, and platforms cannot publish a web page. Open the URL in a private browser window: if you see a page instead of the image or video, upload the file to Pozy instead. POST /v1/tools/validate/media checks a URL for you and reports its type, size and how it compares with each platform's limit (Validation).
| Error | Cause | Fix |
|---|---|---|
400 on presign |
Missing filename, a contentType outside the list, or size over 5 GB. |
Send the real MIME type. |
| Upload refused by storage | uploadUrl is more than an hour old, or the Content-Type differs from the presign call. |
Ask for a new URL and send the same type. |
400 when scheduling |
The upload is more than 7 days old and was never attached to a post. | Upload again. |
Entry failed with user_content |
The file breaks a platform rule (duration, size, ratio). | Check the platform page, or run prepare first. |
#Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/media/presign |
Get upload URL |
POST /v1/media/prepare |
Prepare media for a platform |
| Endpoint | What it does |
|---|---|
POST /v1/tools/validate/media |
Validate media URL |