Skip to main content
The media library holds every image and video your workspace’s posts can use. You add media with one call, POST /workspaces/{workspace}/media_contents: give it a public URL, or send the file itself. The server decides how to finish the upload and tells you with the item’s status. The media library endpoints are available on the Pro and Business plans.

Which way to upload

Both request bodies answer with data, an array of media items, and a message.

Upload from a URL

The file type is read from the file’s bytes, never from the URL or the Content-Type the other server sends. JPEG, PNG, GIF, WebP, MP4, and QuickTime (MOV) are accepted. The stored name is the name (or the URL’s file name) as a slug, plus six random characters and the extension, for example hero-k3j9xq.jpg.

Images come back ready

An image of up to 50 MB is downloaded during the request. The response is 201 with the item active, ready to attach to a post:

Videos come back processing

A video, or any file the other server announces as larger than 50 MB, is downloaded in the background. The response is 202 with the item processing:
Poll GET /media_contents/{mediaContent} with that id until status changes:
  • active: the file is in the library. Its link is final now. While the item was processing, name, full_name, and link could still change.
  • failed: the import did not work. error_message says why, for example that the file was not an image or video, was larger than 2 GB, or could not be downloaded. Fix the cause and import the URL again.
A post can’t use the item until it is active. See Using new media in a post.

Upload files in the request

Send the files as multipart/form-data, each one in files[]:
Don’t set Content-Type yourself: curl’s -F sets multipart/form-data with the boundary.
  • files[]: required, 1 to 10 files per request, up to 200 MB each. Each file’s type is read from its bytes; JPEG, PNG, GIF, WebP, MP4, and QuickTime (MOV) are accepted.
  • label and folder_id: optional, and applied to every file in the request.
Every file is stored during the request, videos included, so the response is 201 with one active item per file, in the order you sent them, and the message Media uploaded successfully!. Each stored name is the uploaded file name as a slug plus six random characters and the extension. If one file is not an image or video, nothing is stored. The response is 422 with an error keyed by the file’s position, for example files.1: Unsupported file type: the file is application/pdf, not an image or video. Supported: JPEG, PNG, GIF, WebP, MP4 and QuickTime.
size, width, height, and time are detected in the background and may come back null right after an upload. Fetch the item again a moment later (GET /media_contents/{mediaContent}) to read them.

Upload errors

A URL that announces no size and turns out larger than 2 GB while downloading ends as failed, with the reason in error_message.

Using new media in a post

Attach an item by its id in the post’s media_ids once its status is active. Creating or updating a post with an item that isn’t ready returns 422:
  • processing: The file for media <id> has not finished importing. Poll GET /media_contents/<id> until its status is active before using this media.
  • failed: The import of media <id> failed (<reason>). Import or upload it again before using this media.
  • temporary (presigned, not uploaded or confirmed): The file for media <id> has not finished uploading. Upload it and confirm the upload before using this media.

Large files: presign, upload, confirm

For a file over the 200 MB limit of the request body, up to 2 GB, upload the bytes straight to storage in three steps. They never pass through the API:
1

Presign

Tell the API which files you want to upload (name, MIME type, size). It pre-creates a temporary media item for each and returns a short-lived, pre-signed upload URL.
2

Upload

PUT each file’s bytes straight to the returned upload_url, replaying the signed headers. This goes to storage, not to the PublishBuddy API.
3

Confirm

Tell the API which uploads finished. It verifies each object landed in storage, moves it into the workspace’s library, and marks it active.

1. Presign

Each entry needs a name and size (bytes); mime is optional but recommended. You can presign up to 10 files per request. Response: one entry per file:
The id is the temporary media item’s UUID. You’ll pass it back in the confirm step. The upload_url is valid for expires_in seconds (30 minutes). Until you confirm, the item has status temporary and the media list leaves it out unless you ask for status=temporary.

2. Upload the bytes

PUT the file straight to upload_url with the headers the URL was signed with. upload_headers maps each of those header names to a list of values. Send every header except Host, with its list of values joined by commas. Your HTTP client sets Host from upload_url on its own:
This request goes to storage, not to api.publishbuddy.com. A 200/204 means the bytes are in place.

3. Confirm

Response: the finalized media items, active and ready to attach to posts:
As with the other uploads, size, width, height, and time may come back null right after confirm. Fetch the item again a moment later to read them. Confirming is all or nothing. If any ID in media_ids has no file in storage, nothing is confirmed and the API returns 422 with an error for each missing file, keyed by its position (media_ids.0, media_ids.1, and so on), for example The file for media 9b1f2c3d-5a6e-7f8a-9b0c-1d2e3f4a5b6c was not found in storage. Upload it to the upload_url from presign, then confirm again. Upload the missing files and send the same request again: an ID that is already confirmed comes back unchanged. A file larger than 2 GB is deleted at this step and reported the same way. Presign and confirm both accept up to 10 items per request: presign every file, PUT each one, then confirm them all together with their IDs in media_ids.

Bulk imports

For hundreds or thousands of files, send them in batches of 10 (the limit per request) and keep each file under 200 MB:
Media that already lives at a public URL is imported one URL per request. Send the requests, collect the id of every 202 answer, and poll those items until each one is active or failed. Send files over 200 MB through presign, upload, confirm. The media_contents endpoint group is on the high-volume tier (1,000 requests per minute per user). Each upload request counts once whatever the number of files, and so do polling requests. With presign, the PUT uploads go to storage and don’t count.

Supported formats and limits

Per-network and per-plan limits may be lower and are enforced at publish time. For very large files (long-form video), upload them once and reuse the same media id across multiple posts.

Organising with folders

Folders are workspace-internal. They help your team find assets and don’t change anything that gets sent to the social network.
Folders are listed one level at a time: GET /workspaces/{workspace}/media_library/folders returns the top level, and the same call with parent_id=<folder-uuid> returns that folder’s children. Deleting a folder (DELETE /media_library/folders/{folder}) also deletes its subfolders and the files inside them, so move files out first if you want to keep them. To upload straight into a folder, pass its folder_id with the upload. To move a file you already have, update its folder_id:

Filtering existing media

Pass folder_id=root for files that are not in any folder. Combine folder_id with page and per_page (see Pagination) to walk large libraries. The list shows active items only. Pass status=processing for URL imports still downloading, status=failed for imports that failed, or status=temporary for presigned uploads not confirmed yet:

Replacing a file

The binary attached to a media object is immutable. To “replace” an asset:
  1. Upload the new file (any of the ways above gives you a new media id).
  2. Update any scheduled (status: waiting) posts that reference the old ID: set their media_ids to include the new ID instead.
  3. Delete the old media object (optional, since already-published posts hold their own reference).
Trying to mutate the file via PUT is intentionally not supported; this keeps the audit trail clean (“the post that went out on Dec 25 used exactly this asset”).

Deleting

Deletion is a soft delete. The file is hidden from listings and can no longer be attached to new posts, but any existing post that used it continues to work, including future scheduled publishes.