Attaching media to Tweets with POST statuses/update_with_media is easy. A few pointers before getting started:
- POST statuses/update_with_media is only available on the upload.twitter.com host. You must use upload.twitter.com for this method and this method alone. Status updates without media should still be performed on api.twitter.com.
- Because the method uses multipart POST, OAuth is handled a little differently.
- POST or query string parameters are not used when calculating an OAuth signature basestring or signature. Only the oauth_* parameters are used. (see examples below)
- Maximum file size is available in the "photo_size_limit" field from GET help/configuration
- Maximum number of media (currently 1) per status update is in the "max_media_per_upload" field from GET help/configuration
- Users have a separate, published daily media upload limit that is indepedent of their unpublished daily status update limits. Details on these limits are communicated in the HTTP headers detailed on POST statuses/update_with_media.
To illustrate the upload process, I've decided to post a status update: "Don't slip up" with a picture of Dick Van Dyke having fallen over an ottoman. The following code sample, which works well with @themattharris' tmhOAuth ( http://github.com/themattharris/tmhOAuth ) demonstrates the process.
-
require '../tmhOAuth.php';
-
require '../tmhUtilities.php';
-
$tmhOAuth = new tmhOAuth(array(
-
'consumer_key' => 'YOUR_CONSUMER_KEY',
-
'consumer_secret' => 'YOUR_CONSUMER_SECRET',
-
'user_token' => 'AN_ACCESS_TOKEN',
-
'user_secret' => 'AN_ACCESS_TOKEN_SECRET',
-
));
-
-
// we're using a hardcoded image path here. You can easily replace this with an uploaded image-see images.php example) -
// 'image = "@{$_FILES['image']['tmp_name']};type={$_FILES['image']['type']};filename={$_FILES['image']['name']}", -
-
$image = "./dickvandyke.jpg';
-
-
$code = $tmhOAuth->request('POST', 'https://upload.twitter.com/1/statuses/update_with_media.json', -
array( -
'media[]' => "@{$image}",
-
'status' => "Don't slip up" // Don't give up..
-
),
-
true, // use auth
-
true // multipart
-
);
-
-
if ($code == 200) {
-
tmhUtilities::pr(json_decode($tmhOAuth->response['response']));
-
} else {
-
tmhUtilities::pr($tmhOAuth->response['response']);
-
}
This process would create a signature base string containing only the OAuth-related fields, as so:
POST&http%3A%2F%2Fupload.twitter.com%2F1%2Fstatuses%2Fupdate_with_media.json&oauth_consumer_key%3DmbmuCGVFTGHZOo5zr5Sx5A%26oauth_nonce%3Df9685243ba64308204b1c14a05950b09%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1313443626%26oauth_token%3D119476949-KQjqYB1QCSC9ZtaTI8RRDDRJdSgk8hMcT4BJMEWi%26oauth_version%3D1.0
Our HTTP connection would look something like (actual multi-part data snipped for readability):
POST http://upload.twitter.com/1/statuses/update_with_media.json HTTP/1.1
User-Agent: themattharris' HTTP Client
Host: upload.twitter.com
Accept: /
Proxy-Connection: Keep-Alive
Authorization: OAuth oauth_consumer_key="mbmuCGVFTGHZOo5zr5Sx5A", oauth_nonce="f9685243ba64308204b1c14a05950b09", oauth_signature="LX%2BcnRvzT5Rm%2BYkPzdhX6I%2Bt9oo%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1313443626", oauth_token="119476949-KQjqYB1QCSC9ZtaTI8RRDDRJdSgk8hMcT4BJMEWi", oauth_version="1.0"
Content-Length: 34411
Content-Type: multipart/form-data; boundary=----------------------------9fa90e137c50
...
In response, the server sends me:
-
{ -
"text":"Don't slip up. http:\/\/t.co\/PjXqx8p",
-
"id_str":"103216165765136386",
-
"in_reply_to_status_id":null,
-
"favorited":false,
-
"in_reply_to_status_id_str":null,
-
"created_at":"Mon Aug 15 21:27:06 +0000 2011",
-
"entities":
-
{ -
"hashtags":
-
[ -
],
-
"media":
-
[ -
{ -
"type":"photo",
-
"url":"http:\/\/t.co\/PjXqx8p",
-
"media_url":"http:\/\/p.twimg.com\/AW6yjk_CQAE0SjQ.jpg",
-
"indices":
-
[ -
15,
-
34 -
],
-
"id_str":"103216165769330689",
-
"expanded_url":"http:\/\/twitter.com\/oauth_dancer\/status\/103216165765136386\/photo\/1",
-
"display_url":"pic.twitter.com\/PjXqx8p",
-
"media_url_https":"https:\/\/p.twimg.com\/AW6yjk_CQAE0SjQ.jpg",
-
"sizes":
-
{ -
"thumb":
-
{ -
"w":150,
-
"h":150,
-
"resize":"crop"
-
},
-
"small":
-
{ -
"w":500,
-
"h":375,
-
"resize":"fit"
-
},
-
"large":
-
{ -
"w":500,
-
"h":375,
-
"resize":"fit"
-
},
-
"medium":
-
{ -
"w":500,
-
"h":375,
-
"resize":"fit"
-
} -
},
-
"id":103216165769330689
-
} -
],
-
"urls":
-
[ -
],
-
"user_mentions":
-
[ -
] -
},
-
"in_reply_to_screen_name":null,
-
"source":"\u003Ca href=\"http:\/\/thisismywebsite.com\" rel=\"nofollow\"\u003ETesting app by episod again\u003C\/a\u003E",
-
"in_reply_to_user_id_str":null,
-
"contributors":null,
-
"place":null,
-
"retweeted":false,
-
"in_reply_to_user_id":null,
-
"geo":null,
-
"user":
-
{ -
"follow_request_sent":false,
-
"contributors_enabled":false,
-
"favourites_count":3,
-
"profile_sidebar_fill_color":"DDEEF6",
-
"url":"http:\/\/bit.ly\/oauth-dancer",
-
"profile_image_url_https":"https:\/\/si0.twimg.com\/profile_images\/730275945\/oauth-dancer_normal.jpg",
-
"location":"San Francisco,
-
CA",
-
"id_str":"119476949",
-
"notifications":false,
-
"profile_background_tile":true,
-
"description":"",
-
"show_all_inline_media":false,
-
"geo_enabled":true,
-
"lang":"en",
-
"created_at":"Wed Mar 03 19:37:35 +0000 2010",
-
"profile_link_color":"0084B4",
-
"profile_image_url":"http:\/\/a2.twimg.com\/profile_images\/730275945\/oauth-dancer_normal.jpg",
-
"listed_count":0,
-
"profile_sidebar_border_color":"C0DEED",
-
"default_profile":false,
-
"time_zone":null,
-
"screen_name":"oauth_dancer",
-
"profile_use_background_image":true,
-
"statuses_count":50,
-
"friends_count":12,
-
"profile_background_color":"C0DEED",
-
"protected":false,
-
"default_profile_image":false,
-
"following":false,
-
"verified":false,
-
"profile_background_image_url":"http:\/\/a0.twimg.com\/profile_background_images\/80151733\/oauth-dance.png",
-
"followers_count":19,
-
"name":"OAuth Dancer",
-
"id":119476949,
-
"is_translator":false,
-
"profile_background_image_url_https":"https:\/\/si0.twimg.com\/profile_background_images\/80151733\/oauth-dance.png",
-
"utc_offset":null,
-
"profile_text_color":"333333"
-
},
-
"retweet_count":0,
-
"coordinates":null,
-
"truncated":false,
-
"id":103216165765136386,
-
"possibly_sensitive":false
-
}
You can find the resultant tweet here: http://twitter.com/#!/oauth_dancer/status/103216165765136386
Let us know how POST statuses/update_with_media works for you and if you encounter any difficulties. Thanks!

Replies
Excellent news. let's try. Thanks!
ahoj
I appreciate posting this along with the sample/headers. Any guidance on when to expect max_media_per_upload to be increased?
{"request":"\/1\/statuses\/update_with_media.json","error":"Missing or invalid url parameter."}
It's possible that my request is a little bit malformed (it's proper HTTP but I'm uploading the status as text/plain) but this error seems like it could be more helpful.
While anything is possible, it's unlikely that max_media_per_upload will be increased anytime soon. Of course, hard coding these values is rarely a good long-term idea.
OK what about guidance on what to do if the user wants to upload more than one picture. Would you guys expect it to post one via Photos and another via another service, or both via another service?
Without the ability to upload multiple images, pic.twitter.com isn't a serious replacement for yfrog and/or twitpic.
Also: what about official apps? Do those get the ability to upload multiple pictures? They currently have that, but are you going to strip that out? In the past we've seen that Twitter likes replacing third party features with their own features...
All apps, Twitter owned or third-party, will only be able to support one photo per Tweet with this new endpoint.
Thanks John, but I meant via other services. For me as an app developer it's important to know what the Twitter apps are going to do. Will the Yfrog/Twitpic implementation in the Twitter for iPhone be removed or will the pic.twitter.com be an addition to the current upload services?
Am I missing something or would an HTTP request be hijack-able if we're not signing the status, etc.. parts of the post? A man in the middle type attack could grab the post and send in its own status/image instead.
Not a huge issue given everyone should be using HTTPS, but just wanted to know if I'm misunderstanding it.
You are understanding that correctly. However, the OAuth 1.0 specification explicitly mentions that you only sign urlencoded formdata.
Or GET parameters, though I'm guessing that'd cause other issues.
Perhaps the docs could use a strong warning about only using this method via HTTPS in a production app?
Actually, posting the status as a GET parameter should work fine, but I doubt you can post the image there too.
Hi. Could I impose on you to post an upload-payload example that is complete except for the binary data of the image being excised, using both the Authentication header method, and the query-string (sent as form data) method? I'm getting a 406/Unacceptable reply on a call that seems correct, but it's easy enough that I'm missing something that a full example would help.
Thanks.
Jeffrey Friedl
I'm trying to use this API from Google API. I am re-using code I had written to POST a photo to Flickr which is also over oauth and with a multipart/form-data format.
Unfortunately, I'm getting "Invalid authorization header" and I absolutely don't understand why.
Here's the full POST data (I only stripped most of the image data):
Making HTTP request:
host = upload.twitter.com
url = https://upload.twitter.com/1/statuses/update_with_media.json
payload = --MsCFc4zoZwMK76tmvFJvTw\r\nContent-Disposition: form-data; name="status"\r\n\r\nCheck out my photo\r\n--MsCFc4zoZwMK76tmvFJvTw\r\nContent-Disposition: form-data; name="media[]"; filename="Photo.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\xff\xd8\xff\xe0\x00\x10...\xd9\r\n--MsCFc4zoZwMK76tmvFJvTw--
headers = {'Content-Length': '184213', 'Accept-Encoding': 'gzip', 'User-Agent': 'AppEngine-Google; (+http://code.google.com/appengine)', 'Host': 'upload.twitter.com', 'Content-Type': 'multipart/form-data; boundary=MsCFc4zoZwMK76tmvFJvTw', 'Authorization': 'OAuth oauth_consumer_key="IkIZwr9DDZvsB97A671LA", oauth_nonce="15952522287459895521", oauth_signature="tk7iEXMK8l1dUtpTNq2nJzTfGqY=", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1313470239", oauth_token="220186573-YZVqXFd4N8w40mteqY3AKoNXs6DcByt7I4ud88Hl", oauth_version="1.0"'}
Any help appreciated!
When you actually send your HTTP headers, do you percent encode them first? For example, the equals ("=") character in your signature should be "%3D"
I got an error "Status is a duplicate" when continuously post different pictures with empty status (or same message such as the same hashtag).
Is this by design? Or, what measures are available?
Do you get this error if you vary the tweet text?
No, of course, the error is not occurred with different text. But, I want to post different pictures with the same tweet text.
This is odd, because on twitter.com, you can have the same tweet text, as the link varies. Something else must be wrong, but I've no idea what the problem could be.
You know what really would be awesome. Switching to OAuth 2.0 :)
What is the current X-MediaRateLimit-Remaining?
Currently, X-MediaRateLimit-Remaining is 30/day, but it's subject to change at any time.
I presume from this we also can't attach media to direct messages either then?
Correct, images can be attached to Tweets, but not to DMs.
Took a look at the photo upload api and I have a slight problem. On a blackberry device, at least for us 3rd party apps ;-), there are pretty small limits on the number of bytes that can reliably be sent in an http POST, typically around 20k. With the Plixi/Lockerz folks we worked around this with an api that allows multiple, sequential http POSTs to upload the photo. I use this same strategy in mypict.me. Its not pretty, but gets the job done.
Is there any chance you guys might add another endpoint for this type of upload?
Couldn't you work around that by using one of your servers as a proxy?
Certainly possible, but incurs substantial costs in terms of bandwidth fees and additional upload time.
Paul -- We were totally unaware of this reality for BlackBerry. We're now discussing this internally. Clearly, this isn't going to be trivial to implement.
Tested it, working fine in server. but return a 0 code in localhost
Servers don't typically send HTTP 0 codes, but some environmental issues can cause it to seem that way. You might be having some SSL issues in your local environment. This link describes a similar scenario: http://stackoverflow.com/questions/4570973/anything-wrong-with-my-curl-code-http-status-of-0
I have no change CURLOPT set and they all as default, they all turn off as 'false'
I had been getting inexplicable "Invalid Unicode value in one or more parameters" errors for all-ASCII tweets, and finally figured it out, so I thought I'd share the unintuitive workaround in case others run into it...
In the "media" multipart section for the photo to upload, you can specify a name for the file. This name is not required by the HTTP standard, nor does it seem to be used by Twitter for anything, but if omitted, you get the "Invalid Unicode value in one or more parameters" error. Setting the name to "foo" (or any non-empty valid utf8 string) avoids the problem.
Twitter folks: is this filename metadata used for anything?
Thanks,
Jeffrey Friedl
Filed a ticket to look into filenames.
I have a Oauth library I already use and I'm trying to figure out what I have to change to get this to work. I changed the domain to upload.twitter.com, and I post the image using the media[] fieldname, and I get back "Error creating status" from the API.
What do I have to change in my CURL call to get this to work? Thanks!
DW
Can you provide an example of the Curl cmd?
An error message of "Error creating status" likely indicates:
- an issue with the image (animated gifs aren't allowed and we have had some issues with some kind of jpegs). Please try with a different image.
- a transient issue with the infrastructure.
We are working on providing additional details in the error messages. Please stay tuned.
Could someone from Twitter describe how image sizes (pixel, not byte) are dealt with? If I upload a photo with, for example, 4000 pixels on the long edge, the largest view I can get via Twitter is 1064 pixels on the long edge. Yet, if I upload an image with 1800 pixels on the long edge, the "large" view is the full 1800-pixels-on-the-long edge size. At what point do you apply a force size reduction? Overall, some best practices for pre-upload image sizing would be appreciated.
Also, what about JPEG quality settings? If Twitter will always force a certain JPEG quality on the largest size made available to the user, I would not want to waste bandwidth uploading with a higher quality setting, so I'd like to hear about what Twitter does here as well.
Finally, what about color spaces? Do you convert everything to sRGB, or do you leave things alone, or do something else? In any case, the version of the image on Twitter has no indication of color space, which means that users are guaranteed to get random color renditions that differ from user to user with no way for the color-savvy user to ensure proper colors for themselves. I would strongly recommend including an embedded color profile if you know the color space of the image, at least for the larger/largest versions presented to the user. If you're not familiar with digital-image color spaces, you need to be: here is a primer: http://regex.info/blog/photo-tech/color-spaces-page1
Thanks.
Jeffrey Friedl
You can see the largest sizes that we currently support in the https://api.twitter.com/1/help/configuration.json call. This is subject to change, but currently we center "crop" the thumbnails, and then "fit" the other images into the given box size:
Ff you have an image larger than 2048x1024, you can scale it down and send it up at the highest, or nearly highest, quality compression and probably save time over sending the higher resolution image. This will likely result in the same result quality and save everyone time and money as well.
We always compress somewhat, and we've played with the quality settings a few times. There's no easy answer on this part. Send the highest quality image that your latency budget can tolerate.
On color spaces: We're having a minor corner-case problem with PNG color space conversion stuff, and until that's resolved, I don't know how color management will all turn out. Once I understand what the issues are, and how we're fixing them, I'll try to remember to come back and update this thread.
Thanks much, John.
I'm not seeing these limits enforced, and am wondering if it's really just a misunderstanding on my part. Here's an image that's 2000 pixels wide, by 1331 pixels tall:
https://p.twimg.com/AXFKt0TCEAAKP4B.jpg:large
Based upon the 1024-wide by 2048-tall limit you referenced, I would have expected this wider-than-tall image to be drastically reduced so that it was 1024 pixels wide (and 681 pixels tall).
In doing the test, I was hoping to find out that the limits weren't actually hard-coded to the width/height numbers mentioned, but instead that they were "2048 on the long edge, 1024 on the short edge", but the result I'm actually seeing is not possible with either interpretation, so I'm quite confused.
My goal in asking is to know what kind of pixel-size limit to enforce in my app (for Lightroom) before uploading to Twitter. Users of this app will often want to use the largest size that Twitter will accept.
Looking into this. Something seems wrong, but I can't figure it out at this early hour. Will get back to this thread with an update.
Would it be possible to include the user's photo-upload quota data (comperable to the X-Mediaratelimit-Remaining in the photo-upload reply header) in a non-upload call, such as account/verify_credentials? Then a client could keep a user informed of their status even before a photo-upload.
Thanks,
Jeffrey Friedl
I filed a ticket to look into adding this field to other endpoints.
Is this done yet? Would be great to know if it is.
Not done yet. Pretty far down on the untriaged queue.
What about the issues several people seem to be having with error 500s coming from this API? i.e. https://dev.twitter.com/discussions/1525
Hi, I tried to implement the new API in my WP7 photo effect app. The request looks OK to me, but I get a response "error=Could not authenticate with OAuth.".
I wonder why? The Auth header looks good or what is wrong here?
Here's the request (I truncated the image date):
POST https://upload.twitter.com/1/statuses/update_with_media.json?oauth_consumer_key=0Pf1TSjyOFYZZoOAQtcdvQ&oauth_token=226570571-EhNbpgfDhyakp0T9u030hurFi27iwV3t8TRcvfN4&oauth_nonce=vf28ogfpjw5kwjop&oauth_timestamp=1313614615&oauth_signature_method=HMAC-SHA1&oauth_signature=njSbdKjELfSIpoBpGe3o5m56vck%3D&oauth_version=1.0 HTTP/1.1
Accept: /
Referer: file:///Applications/Install/DE2C9557-9ABC-4B74-ACC7-6D4ADB19B575/Install/
Content-Length: 122360
Accept-Encoding: identity
Authorization: OAuth oauth_consumer_key="0Pf1TSjyOFYZZoOAQtcdvQ",oauth_nonce="vf28ogfpjw5kwjop",oauth_signature="njSbdKjELfSIpoBpGe3o5m56vck%3D",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1313614615",oauth_token="226570571-EhNbpgfDhyakp0T9u030hurFi27iwV3t8TRcvfN4",oauth_version="1.0"
Content-Type: multipart/form-data; boundary=5abd2fbd-280b-4c5a-8705-507ae24b695b
User-Agent: NativeHost
Host: upload.twitter.com
Connection: Keep-Alive
Cache-Control: no-cache
--5abd2fbd-280b-4c5a-8705-507ae24b695b
Content-Disposition: form-data; name="status"
--5abd2fbd-280b-4c5a-8705-507ae24b695b
Content-Disposition: form-data; name="media[]"; filename="No_FX_2011-08-17_22-56-55.jpg"
Content-Type: image/jpeg
?????Exif??MM?*?????i??????2???????&???????????
...
I noticed a few things while reading through all this (going from the top of the request to the bottom).
Thanks, I'm not an expert in HTTP so I'm using a REST library for this :) and it works just fine with other Twitter API or TwitPic and Lockerz. I compared my request with the sample at the bottom of this blog post here. They seem similar. Only the URL params are additionally part of my request, but could this really be the issue?
Yes
Thanks for your help! :) The URL parameters were indeed the issue. Works like a charm now.
Actually it's too bad it matters if they're present or not. Why does Twitter don't just skip those.