Skip to main content
Posts with media perform better on every platform. Postbreeze uses presigned URLs so your file goes directly from your code to storage — the API server never touches the bytes. Uploaded media is account-global — once a file is uploaded, the same publicUrl works across every workspace and every social account your key can reach. Upload once, reuse anywhere.

Step 1: Get a presigned URL

POST /media/presign with just filename + contentType. No workspace, no file size required.
const { uploadUrl, publicUrl, headers } = await postbreeze.media.presign({
  filename: "hero.jpg",
  contentType: "image/jpeg",
});
Response:
{
  "uploadUrl": "https://<r2-host>/users/usr_…/med_abc/hero.jpg?X-Amz-…",
  "publicUrl": "https://media.postbreeze.ai/users/usr_…/med_abc/hero.jpg",
  "expiresAt": "2026-06-15T09:15:00.000Z",
  "headers": { "Content-Type": "image/jpeg" }
}
The uploadUrl is valid for 15 minutes. Issue the PUT promptly, or re-presign.

Step 2: Upload the file

A direct PUT to uploadUrl. The signature is in the URL — no Authorization header needed on this request.
import { readFile } from "node:fs/promises";

const fileBytes = await readFile("./hero.jpg");

await fetch(uploadUrl, {
  method: "PUT",
  body: fileBytes,
  headers, // { "Content-Type": "image/jpeg" }
});
A 200 OK from R2 means the bytes are stored. Your publicUrl is now live.

Step 3: Use in a post

Pass publicUrl into the post’s mediaItems array:
await postbreeze.posts.create({
  content: "Launching today 🚀",
  platforms: [{ accountId: "soc_…" }],
  mediaItems: [{ url: publicUrl, type: "image" }],
});
type can be "image", "video", "gif", or "document" (uppercase also works).

Same media, many platforms

When mediaItems is at the post level, it’s used by every platform listed in platforms[]:
await postbreeze.posts.create({
  content: "Same image, three platforms.",
  platforms: [
    { accountId: "soc_ig_123" },
    { accountId: "soc_x_456" },
    { accountId: "soc_tiktok_789" },
  ],
  mediaItems: [{ url: publicUrl, type: "image" }],
});
One upload, one publicUrl, every platform — no per-platform duplication.

Different media per platform

Sometimes you need a square crop for Instagram and a landscape one for YouTube. Override per-target with mediaIds:
await postbreeze.posts.create({
  caption: "Cross-platform release",
  targets: [
    {
      socialAccountId: "soc_yt_…",
      mediaIds: [landscapeMediaId], // landscape video
    },
    {
      socialAccountId: "soc_tiktok_…",
      mediaIds: [portraitMediaId], // portrait video
    },
  ],
});

Attach a file you already have on a CDN

If your image already lives on a public HTTPS URL, you can skip the presign + PUT entirely and paste the URL straight into mediaItems:
await postbreeze.posts.create({
  content: "Launching today 🚀",
  platforms: [{ accountId: "soc_…" }],
  mediaItems: [
    { url: "https://cdn.example.com/hero.jpg", type: "image" },
  ],
});
Postbreeze will fetch the URL server-side (SSRF-guarded) and store the bytes — same outcome as a direct upload.

Supported formats

TypeFormatsMax size
ImagesJPG, PNG, GIF, WebP, HEIC, HEIF5 GB
VideosMP4, MOV, AVI, WebM5 GB
DocumentsPDF (LinkedIn only)100 MB
The size limits above are the storage ceiling. Each platform enforces its own (much tighter) caps at publish time — see per-platform limits below. The server accepts files up to the storage ceiling so you can keep originals alongside re-encoded variants; the publisher rejects files that exceed the destination platform’s limit.

Per-platform limits

Each platform enforces its own media rules at publish time:
PlatformImagesVideosDocumentsNotes
Instagramup to 10 (carousel)19:16 for Reels, 4:5–1.91:1 for Feed
Facebookup to 10 (carousel)1
Xup to 41Can’t mix images + video in one post
LinkedInup to 2011 PDF (carousel)The only platform that accepts PDFs
TikTokup to 35 (carousel)1
Threadsup to 20 (mixed)1
Pinterestup to 5 (carousel)0
YouTube01Video only
See each platform guide for full per-platform rules.

LinkedIn PDFs

LinkedIn’s “document carousel” surface accepts a single PDF. Upload it the same way as any other file, then attach with type: "document":
const { uploadUrl, publicUrl, headers } = await postbreeze.media.presign({
  filename: "deck.pdf",
  contentType: "application/pdf",
});

await fetch(uploadUrl, { method: "PUT", body: pdfBytes, headers });

await postbreeze.posts.create({
  caption: "Our Q3 deck — page-by-page.",
  targets: [
    {
      socialAccountId: "soc_linkedin_…",
      mediaItems: [{ url: publicUrl, type: "document" }],
    },
  ],
});
If you put a PDF in the post-level mediaItems for a cross-post that includes non-LinkedIn targets, Postbreeze rejects the post with a 400 — PDFs only land on LinkedIn. Use per-target mediaItems (or mediaIds) to send a different asset to the other platforms.