I’m in Alma, NB, Canada 🇨🇦  |  ☀️ It’s 22° at 3:04pm

Screenshot of Mastodon update with media attached

Using PHP and cURL to post media to the Mastodon API

A couple of years ago I posted the most popular article on the site on how to use PHP and cURL to post to the Mastodon API. Since then I’ve been meaning to post a follow-up -on how to post media and finally it’s here.

If you haven’t read the first part of this series and don’t know how to post to the Mastodon API, you should read that first. There’s a link back to this article at the end of that one.

Hopefully the code below is commented enough to make sense. It’s a bit tricky with sending using multipart-form-data and its boundaries.

$bearer_token = [YOUR ACCESS TOKEN GOES HERE];
$media_sleep = false;
$image = "test.jpg";
$alt_text = "An amazing test picture of a sunset";

// the main status update array, this will have media IDs added to it further down
// and will be used when you send the main status update using steps in the first article
$status_data = array(
  "status" => $status_message,
  "language" => "eng",
  "visibility" => "public"

// if we are posting an image, send it to Mastodon
// using a single image here for demo purposes
if ($image !== "") {
  // enter the alternate text for the image, this helps with accessibility
  $fields = array(
    "description" => $alt_text

  // get location of image on the filesystem
  $imglocation = "/path/on/server/to/$image";

  // add images to files array, this is a single image for demo
  $files = array();
  $files[$image] = file_get_contents($imglocation);

  // make a multipart-form-data delimiter
  $boundary = uniqid();
  $delimiter = '-------------' . $boundary;

  $post_data = '';
  $eol = "\r\n";

  foreach ($fields as $name => $content) {
    $post_data .= "--" . $delimiter . $eol . 'Content-Disposition: form-data; name="' . $name . "\"" . $eol . $eol . $content . $eol;

  foreach ($files as $name => $content) {
    $post_data .= "--" . $delimiter . $eol . 'Content-Disposition: form-data; name="file"; filename="' . $name . '"' . $eol . 'Content-Transfer-Encoding: binary' . $eol;
    $post_data .= $eol;
    $post_data .= $content . $eol;

  $post_data .= "--" . $delimiter . "--".$eol;

  $media_headers = [
    "Authorization: Bearer $bearer_token",
    "Content-Type: multipart/form-data; boundary=$delimiter",
    "Content-Length: " . strlen($post_data)

  // send the image using a cURL POST
  $ch_media_status = curl_init();
  curl_setopt($ch_media_status, CURLOPT_URL, "$instance_url/api/v2/media");
  curl_setopt($ch_media_status, CURLOPT_POST, 1);
  curl_setopt($ch_media_status, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch_media_status, CURLOPT_HTTPHEADER, $media_headers);
  curl_setopt($ch_media_status, CURLOPT_POSTFIELDS, $post_data);
  $media_response = curl_exec($ch_media_status);
  $media_output_status = json_decode($media_response);
  $media_info = curl_getinfo($ch_media_status);
  curl_close ($ch_media_status);
  $http_code = $media_info['http_code'];

  // check the return status of the POST request
  if (($http_code == 200) || ($http_code == 202)) {
    $status_data['media_ids'] = array($media_output_status->id); // id is a string!
    $post_to_mastodon = true;

    if ($http_code == 200) {
      // 200: MediaAttachment was created successfully, and the full-size file was processed synchronously (image)        
      $media_sleep = false;
    else if ($http_code == 202) {
      // 202: MediaAttachment was created successfully, but the full-size file is still processing (video, gifv, audio)
      // Note that the MediaAttachment’s url will still be null, as the media is still being processed in the background
      // However, the preview_url should be available
      $media_sleep = true;
    else {
      $post_error_message = "Error posting media file";
  else {
    $post_error_message = "Error posting media file, error code: " . $http_code;

// wait for the complex media to finish processing on server
// this is only so when the status is posted the video can be watched right away
if ($media_sleep) {

// continue with posting the update using steps and from first article
// ...

At this point your media has been uploaded successfully and you have the list of media ID’s to send as part of the main status update covered in the first article. In that article, you’ll be sending another cURL request following those steps, except your $status_data array will have a media ID’s item in it.

One trick I learned is when sending a status update that contains media ID’s, use JSON to send the data instead of the default multipart-form-data. So from the first article you should update the $headers array to include the JSON content type, and convert the $status_data array to JSON like this, before sending the cURL request:

$bearer_token = [YOUR ACCESS TOKEN GOES HERE];

// add a JSON content type to the headers
$headers = [
  "Authorization: Bearer $bearer_token",
  'Content-Type: application/json'

// JSON-encode the status_data array
$post_data = json_encode($status_data);

// Initialize cURL with headers and post data
$ch_status = curl_init();
curl_setopt($ch_status, CURLOPT_URL, "$instance_url/api/v1/statuses");
curl_setopt($ch_status, CURLOPT_POST, 1);
curl_setopt($ch_status, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch_status, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch_status, CURLOPT_POSTFIELDS, $post_data); // send the JSON data

// Send the JSON data via cURL and receive the response
$output_status = json_decode(curl_exec($ch_status));

// Close the cURL connection
curl_close ($ch_status);

The API call will return an Attachment in JSON (in this case in the $output_status variable).

I hope this helps you sending status updates with media to the Mastodon API. More information on attaching media to a status can be found on the API documentation for media page.