# Hosting Media

When you attach an image or video 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.

## Why media must be hosted

The `url` field on each asset (`image` or `video`) 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.

> **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 `createPost` but 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](https://cloudinary.com/) - 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](https://www.cloudflare.com/developer-platform/products/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:

```graphql
assets: [
  {
    image: {
      url: "https://your-host.example.com/photo.jpg"
    }
  }
]
```

Video - you can optionally provide a `thumbnailUrl` (also a publicly hosted URL) for the video's poster image:

```graphql
assets: [
  {
    video: {
      url: "https://your-host.example.com/clip.mp4"
      thumbnailUrl: "https://your-host.example.com/clip-thumb.jpg"
    }
  }
]
```

A full mutation looks like this:

```graphql
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](/examples/create-image-post.html) and [Create a video post](/examples/create-video-post.html) examples.

## Troubleshooting

If a media URL can't be fetched, the mutation returns a `MutationError` with a message:

```json
{
  "data": {
    "createPost": {
      "message": "Failed to create post: Failed to fetch image dimensions: Not Found"
    }
  }
}
```

If you hit this, check the URL against this list:

1. Public - open it in an incognito window; it should load with no login.
2. Direct - it points at the file itself, not a share, preview, or redirect page.
3. HTTPS - it's served over `https://`.
4. Still live - it isn't a signed/expiring URL that may lapse before a scheduled post publishes.
