Media

Media assets — images, videos, GIFs, audio, and documents — are uploaded directly from your client to object storage using a presigned URL, so large files never pass through our API. The flow is three steps: request an upload URL, PUT the bytes to that URL, then confirm the upload. Once a media asset is ready, reference its id from a post container.

Media has a 60-day retention window: each asset is automatically purged 60 days after it was created. Reference it from posts within that window, and re-upload if you need it again later.

The media object

FieldTypeDescription
idstringUnique identifier for the asset.
kindenumOne of image, video, gif, audio, document.
filenamestringOriginal filename supplied at upload time.
content_typestring | nullMIME type of the asset.
sizenumber | nullSize in bytes, populated on confirm.
statusenumThe asset’s lifecycle state — see Media status below for the full enum.
width / heightnumber | nullPixel dimensions for image and video assets.
duration_msnumber | nullDuration in milliseconds for time-based media.
created_attimestampWhen the asset record was created.
urlstringPublic URL of the asset (returned on confirm and retrieve).
thumbnail_urlstring | nullPublic URL of the asset’s thumbnail/cover. For images this is the image itself; for videos it’s a generated frame (null until processing completes).
thumbnailobject | nullThe thumbnail derivative: { url, width, height, frame_time_ms, source }. source is generated, custom, or original.

Media status

An asset moves through a fixed set of states from upload to publish-ready. Only a ready asset is safe to attach to a post; videos in particular must be fully ready (their cover frame extracted) before they publish.

statusMeaning
pending_uploadThe record exists and an upload URL was issued, but no bytes have landed. The state right after POST /media/upload.
uploadingA resumable (chunked) upload session is streaming bytes to storage; the object isn’t complete yet.
uploadedThe bytes are fully in storage but the asset hasn’t been confirmed/processed yet. 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.
readyPublish-ready: in storage, within its size cap, and (for video) with a generated cover. url and thumbnail_url are populated.
failedProcessing failed terminally (e.g. a corrupt/unreadable video). Re-upload to recover.
deletedSoft-deleted via DELETE /media/{id}; excluded from lists and not attachable to posts.

How an asset reaches ready: after POST /media/upload (state pending_upload) you PUT the bytes, then call POST /media/{id}/confirm. Confirm verifies the object, records its size, and re-checks the size cap. Images and GIFs become ready immediately; videos return processing while a cover frame is extracted, then flip to ready (poll GET /media/{id}). A resumable upload additionally reports uploading/uploaded while chunks are in flight. For the full lifecycle diagram and the per-platform media-requirements matrix, see Media processing & limits.

Thumbnails & covers

Thumbnails are a media-level derivative, not a per-platform setting. When you confirm a video upload, the asset enters processing while we extract a default cover frame; once done it becomes ready and exposes a thumbnail_url. Images are their own thumbnail and are ready immediately. The active thumbnail is then reused automatically at publish time— as the Instagram Reels cover, YouTube custom thumbnail, Facebook video cover, Pinterest video-pin cover, and TikTok cover frame — so you don’t set a cover per platform.

POST /media/{id}/thumbnail

Set or regenerate a thumbnail. Pass source_media_id (an existing image asset) to use a custom cover, or frame_time_ms to regenerate from a specific video frame (the two are mutually exclusive; an empty body regenerates the default frame). The asset re-enters processing and the new thumbnail becomes primary once the job runs.

curl
# Use an uploaded image as a custom video cover
curl https://api.postfuze.com/api/v1/media/m_video123/thumbnail \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "source_media_id": "m_cover456" }'

# Or regenerate from a specific frame (1.5s in)
curl https://api.postfuze.com/api/v1/media/m_video123/thumbnail \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "frame_time_ms": 1500 }'
202 Accepted
{ "id": "m_video123", "status": "processing" }

Request an upload URL

POST /media/upload

Creates a media record in pending_upload state and returns a presigned URL to PUT the file bytes to. The kind is inferred from the content type or filename when not supplied. The returned method and headers must be used exactly when performing the upload.

ParameterTypeRequiredDescription
filenamestringYesOriginal filename (1–255 characters).
content_typestringNoMIME type. Used both for the presigned URL and to infer kind.
kindenumNoOverride the inferred kind: image, video, gif, document, audio.
tenant_idstringNoAssociate the asset with one of your end tenants.
modeenumNoUpload mode: single (default) returns one presigned PUT with the size cap bound into the signature; resumable returns a chunked upload session for large media (the cap is enforced at confirm instead).

Per-kind upload size caps

Each kind has a maximum upload size — image 30 MB, gif 50 MB, video 5 GB, document 100 MB, audio 200 MB. In single mode the cap is bound into the presigned signature (as an x-goog-content-length-range constraint), so storage rejects an over-cap PUT with 413; it is also re-checked against the object’s real size at confirm. Use mode: "resumable" for large files. These are the bytes PostFuze accepts at upload; individual platforms cap the same kind lower — see Media processing & limits.
curl
curl https://api.postfuze.com/api/v1/media/upload \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "hero.png",
    "content_type": "image/png"
  }'
201 Created
{
  "id": "m_9f8e7d6c",
  "upload_url": "https://storage.googleapis.com/postfuze-media/org/…/m_9f8e7d6c/hero.png?X-Goog-Signature=…",
  "method": "PUT",
  "headers": { "Content-Type": "image/png" },
  "expires_at": "2026-06-09T12:25:00.000Z"
}

Upload the bytes to the presigned URL using the returned method and headers:

curl
curl -X PUT "https://storage.googleapis.com/postfuze-media/org/…/m_9f8e7d6c/hero.png?X-Goog-Signature=…" \
  -H "Content-Type: image/png" \
  --data-binary @hero.png

Confirm an upload

POST /media/{id}/confirm

Verifies the object exists in storage (via a HEAD request), records its size, and returns the media object with its public url. Images become ready immediately; videos return status: "processing" while a cover frame is extracted (poll GET /media/{id} until ready, then thumbnail_url is populated). Call this after the PUT completes. If the object is not found, the request fails with 400; if the stored object exceeds its kind’s size cap it is deleted and the request fails with 413 Payload Too Large (carrying max_bytes and size).

curl
curl https://api.postfuze.com/api/v1/media/m_9f8e7d6c/confirm \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{}'
200 OK
{
  "id": "m_9f8e7d6c",
  "kind": "image",
  "filename": "hero.png",
  "content_type": "image/png",
  "size": 184213,
  "status": "ready",
  "width": null,
  "height": null,
  "duration_ms": null,
  "created_at": "2026-06-09T12:24:00.000Z",
  "url": "https://cdn.postfuze.com/org/…/m_9f8e7d6c/hero.png",
  "thumbnail_url": "https://cdn.postfuze.com/org/…/m_9f8e7d6c/hero.png",
  "thumbnail": { "url": "https://cdn.postfuze.com/org/…/m_9f8e7d6c/hero.png", "width": null, "height": null, "frame_time_ms": null, "source": "original" }
}

Retrieve a media asset

GET /media/{id}

Returns a single media asset by id, including its public url. Returns 404 if not found or deleted.

curl
curl https://api.postfuze.com/api/v1/media/m_9f8e7d6c \
  -H "Authorization: Bearer sk_live_…"
200 OK
{
  "id": "m_9f8e7d6c",
  "kind": "image",
  "filename": "hero.png",
  "content_type": "image/png",
  "size": 184213,
  "status": "ready",
  "width": null,
  "height": null,
  "duration_ms": null,
  "created_at": "2026-06-09T12:24:00.000Z",
  "url": "https://cdn.postfuze.com/org/…/m_9f8e7d6c/hero.png"
}

List media

GET /media

Returns your media assets, newest first. Deleted assets are excluded. This endpoint uses offset pagination — pass limit (default 25, max 100) and offset, and filter by kind or status. The response carries a pagination block with the page parameters and the total matching count. See Pagination & filtering.

curl
curl "https://api.postfuze.com/api/v1/media?kind=image&limit=25&offset=0" \
  -H "Authorization: Bearer sk_live_…"
200 OK
{
  "data": [
    {
      "id": "m_9f8e7d6c",
      "kind": "image",
      "filename": "hero.png",
      "content_type": "image/png",
      "size": 184213,
      "status": "ready",
      "width": null,
      "height": null,
      "duration_ms": null,
      "created_at": "2026-06-09T12:24:00.000Z"
    }
  ],
  "pagination": { "limit": 25, "offset": 0, "total": 137 }
}

Delete a media asset

DELETE /media/{id}

Soft-deletes the asset and sets its status to deleted. Returns 404 if not found or already deleted.

curl
curl -X DELETE https://api.postfuze.com/api/v1/media/m_9f8e7d6c \
  -H "Authorization: Bearer sk_live_…"
200 OK
{
  "id": "m_9f8e7d6c",
  "deleted": true
}