Hosting Media
When you attach an image, video, or document to a post, the Buffer API doesn't accept a file upload - there's no upload endpoint. Instead, you host the file yourself and pass a publicly accessible URL in the assets array. The thumbnail on a link attachment works the same way.
Why media must be hosted
The url field on each asset (image, video, or document) must point to a file that is reachable over the public internet without authentication.
This means the URL must work for anyone, not just you. Links that require a viewer to be signed in - for example a Google Drive or Dropbox "share" link - will not work.
Some assets take more than one URL. A document needs both the file and a thumbnail, and a link attachment can carry its own thumbnail. Every URL you pass has to meet the same bar, not just the main one.
The URL must stay reachable until the post publishes, not just when you create it. Buffer fetches the media when the post goes out, which for scheduled or queued posts can be hours or days later. Avoid expiring or signed URLs (such as S3 pre-signed links or Cloudinary signed-delivery URLs) - they often work the moment you call
createPostbut expire before the post publishes, causing it to fail silently. Use a stable, permanent URL.
Where to host your media
You can use any host that serves files at a direct, public URL. If you don't already have one, these options have a free tier and work well:
- Cloudinary - A media management platform with a generous free tier. After you upload a file you get a direct, publicly accessible URL you can use right away. It also offers optional on-the-fly image and video transformations. Use the delivery URL (e.g.
https://res.cloudinary.com/...), not the link to the dashboard or media-library page. - Cloudflare R2 - Object storage with a free tier. A good fit if you already use Cloudflare or are comfortable with a slightly more technical setup. Upload your file and enable public access on the bucket (or attach a public custom domain) so the file is reachable without credentials.
Verifying that a URL works
Before using a URL with the API, open it in a private/incognito browser window:
- If the file loads directly without asking you to log in, it will work with the API.
- If you see a login prompt, a preview page, or an error, the URL won't work - host the file somewhere that serves it directly.
A good media URL is:
- Public - loads without authentication
- Direct - points straight at the file (not a redirect or preview page)
- HTTPS - served over
https:// - Stable - won't expire before the post publishes
Using the media URL
Once your file is hosted, pass its URL in the assets array on createPost or editPost. assets is an ordered list where each entry specifies exactly one asset.
Image - pass the publicly hosted image URL. You can optionally pass thumbnailUrl as well, which is a second hosted URL and needs to be just as public and stable as the image itself:
assets: [
{
image: {
url: "https://your-host.example.com/photo.jpg"
}
}
]
Video - pass the publicly hosted video URL. To set the video thumbnail, choose which frame to use with metadata.thumbnailOffset (a millisecond offset into the video) on Instagram, TikTok, or Pinterest:
assets: [
{
video: {
url: "https://your-host.example.com/clip.mp4"
metadata: { thumbnailOffset: 2000 }
}
}
]
Document - pass both the file URL and a thumbnail URL, plus a title. All three are required, so a document post means hosting two files, not one:
assets: [
{
document: {
url: "https://your-host.example.com/report.pdf"
thumbnailUrl: "https://your-host.example.com/report-cover.jpg"
title: "Q3 report"
}
}
]
A full mutation looks like this:
mutation CreatePost {
createPost(
input: {
text: "Check out our latest update!"
channelId: "some_channel_id"
schedulingType: automatic
mode: addToQueue
assets: [
{
image: {
url: "https://your-host.example.com/photo.jpg"
}
}
]
}
) {
... on PostActionSuccess {
post { id }
}
... on MutationError {
message
}
}
}
For complete walkthroughs, see the Create an image post and Create a video post examples.
Link attachments
A link card is not an asset. To attach one, use metadata.{service}.linkAttachment, which is supported on Bluesky, Facebook, LinkedIn, Substack, and Threads:
metadata: {
linkedin: {
linkAttachment: {
url: "https://example.com/announcement"
title: "Our latest announcement"
description: "What we shipped this quarter"
thumbnail: { url: "https://your-host.example.com/card.jpg" }
}
}
}
Two things to know:
- The
thumbnail.urlis fetched by Buffer the same way asset URLs are, so it has to meet every point in the list above. The link's ownurlis the page you're linking to and is not fetched as media. linkAttachmentis mutually exclusive with a non-emptyassetsarray. Passing both is rejected, so a post is either a link card or a media post, never both.
Troubleshooting
If a media URL can't be fetched, the mutation returns a MutationError with a message:
{
"data": {
"createPost": {
"message": "Failed to create post: Failed to fetch image dimensions: Not Found"
}
}
}
If you hit this, check the URL against this list:
- Public - open it in an incognito window; it should load with no login.
- Direct - it points at the file itself, not a share, preview, or redirect page.
- HTTPS - it's served over
https://. - Still live - it isn't a signed/expiring URL that may lapse before a scheduled post publishes.
If the post has a document or a link attachment, check the thumbnail URL against the same list. A thumbnail that can't be fetched fails the whole mutation.