Media processing & limits

Every image, video, GIF, audio file, and document you publish is first uploaded as a media asset — directly from your client to object storage via a presigned URL, so large files never pass through the API. This page covers the asset lifecycle (thestatus an asset moves through from upload to publish-ready) and the per-platform media requirements we validate against before a post is accepted. For the upload/confirm endpoints themselves, see the Media API.

The asset lifecycle

An asset is created in pending_upload when you request an upload URL, and reaches ready once the bytes are in storage and any processing (video cover-frame extraction) has finished. Only a ready asset — or a still-processing one that is otherwise valid — can be attached to a post; videos must be fully ready before they publish.

Created
pending_upload
Bytes in flight
uploadinguploaded
Confirmed
processing
Publish-ready
ready
Terminal
faileddeleted
Images and GIFs go straight to ready on confirm; videos pass through processing while a cover frame is extracted. uploading/uploaded are transient states a resumable, chunked upload reports while bytes are in flight. failed and deleted are terminal.
statusMeaning
pending_uploadThe record exists and an upload URL has been issued, but no bytes have landed yet. The default state right after POST /media/upload.
uploadingA resumable (chunked) upload session is in progress — bytes are streaming to storage but the object is not yet complete.
uploadedThe object bytes are fully in storage but the asset has not yet been confirmed/processed. It is usable as a custom thumbnail source at this stage.
processingConfirmed and being prepared: a video’s cover frame is extracted, or a thumbnail is being (re)generated. Set on confirm for videos and after POST /media/{id}/thumbnail.
readyThe asset is publish-ready: present in storage, within its size cap, and (for video) with a generated cover. Its url and thumbnail_url are populated.
failedProcessing failed terminally (e.g. an unreadable/corrupt video). Re-upload to recover.
deletedSoft-deleted via DELETE /media/{id}; excluded from list results and not attachable to posts.

How an asset reaches ready

After POST /media/upload the asset is pending_upload. You PUT the bytes to the presigned URL, then call POST /media/{id}/confirm. Confirm verifies the object exists, records its size, and re-checks the size cap. From there the path branches by kind:

  • Images & GIFs become ready immediately on confirm — an image is its own thumbnail.
  • Videos return processing on confirm while a cover frame is extracted asynchronously; poll GET /media/{id} until it flips to ready, at which point thumbnail_url is populated.

A resumable upload (see below) additionally surfaces uploading while chunks stream and uploaded once they all land; you still call confirm to advance it into processing/ready. See Media for the full request/response shapes and thumbnail controls.

Upload size caps

Each media kind has a maximum upload size. The cap is enforced twice: it is bound into the presigned PUT signature (as an x-goog-content-length-range constraint, so storage rejects an over-cap upload with a 400), and re-checked against the stored object’s real size at confirm — so a backend that doesn’t honor the range still can’t let an oversized object through. An over-cap object is rejected at confirm with 413 Payload Too Large (carrying max_bytes and size) and deleted from storage.

KindMax upload size
image30 MB
gif50 MB
video5 GB
document100 MB
audio200 MB

Resumable uploads for large media

The default upload mode is single — one presigned PUT with the cap bound into the signature. For large files, request mode: "resumable" on POST /media/upload to get a chunked upload session instead; the byte ceiling is not bound into the resumable session, so the confirm HEAD check is the authoritative size gate. These per-kind caps are the upper bound PostFuze accepts at upload; an individual platform may accept far less for the same kind — see the matrix below.

Per-platform media requirements

Before a post is accepted, attached media is validated against the target platform’s published constraints. The conservative public limits per platform are below — where a platform tiers limits (e.g. verified vs. standard accounts), PostFuze encodes the standard tier so a passing asset is safe for every account class. Counts are the maximum attachable to one post; aspect bounds are width-to-height ratios.

Images

PlatformMax imagesMax size eachFormats
X (Twitter)45 MBJPEG, PNG, GIF, WebP
LinkedIn910 MBJPEG, PNG, GIF
Facebook1010 MBJPEG, PNG, GIF, WebP
Instagram108 MBJPEG, PNG
TikTok (photo mode)3520 MBJPEG, WebP
YouTubeVideo only (no image post type)
Pinterest120 MBJPEG, PNG, WebP
Threads208 MBJPEG, PNG
Bluesky4~1 MBJPEG, PNG, GIF, WebP
Google Business110 MBJPEG, PNG

Video

PlatformMax sizeDurationFormatsAspect (w:h)
X (Twitter)512 MB0.5 s – 140 sMP4, MOV1:3 – 3:1
LinkedIn5 GB3 s – 30 minMP4~1:2.4 – 2.4:1
Facebook4 GB1 s – 240 minMP4, MOV9:16 – 16:9
Instagram1 GB3 s – 15 minMP4, MOV9:16 – 1.91:1
TikTok4 GB3 s – 10 minMP4, MOV, WebM9:16 – 16:9
YouTube128 GB1 s – 12 h*MP4, MOV, WebM, MKV9:16 – 16:9
Pinterest2 GB4 s – 15 minMP4, MOV, M4V1:2 – 2:1
Threads1 GB1 s – 5 minMP4, MOV9:16 – 16:9
Bluesky50 MB1 s – 60 sMP4, MOV1:3 – 3:1
Google Business75 MB1 s – 30 sMP4, MOV9:16 – 16:9

* YouTube accepts up to 12 h / 128 GB at the API, but unverified accounts are capped at 15 minutes; verify the channel to lift it. Across all platforms, video is accepted with H.264 (and, where supported, HEVC) codecs.

One asset, many platforms

When you fan a single post out to several accounts, the same media asset must satisfy eachtarget’s constraints — a 1.5 GB video bound for X (512 MB cap) is rejected for that target even though it’s fine for LinkedIn. Size the asset for the strictest platform you publish it to, or split the post per platform. See Posts for how per-target validation surfaces.

Example: poll a video to ready

await-ready.ts
async function waitForReady(mediaId: string): Promise<void> {
  for (;;) {
    const res = await fetch(`https://api.postfuze.com/api/v1/media/${mediaId}`, {
      headers: { Authorization: `Bearer ${process.env.POSTFUZE_API_KEY}` },
    });
    const media = (await res.json()) as { status: string };
    if (media.status === 'ready') return;
    if (media.status === 'failed' || media.status === 'deleted') {
      throw new Error(`media ${mediaId} ended in ${media.status}`);
    }
    // pending_upload / uploading / uploaded / processing — keep polling.
    await new Promise((r) => setTimeout(r, 2000));
  }
}