Any.do Unofficial API

    I'm in the process of moving away from a Mac to Linux, and one of the things that I'm looking for is a desktop application that manages my tasks and also syncs with an Android application. In my opinion, there is no better Android task application than Any.do, however the only thing that they have that even remotely resembles a desktop application is a Chrome extension. While that's helpful, it doesn't provide the level of desktop integration that I prefer, so I decided to try to write one.

    Unfortunately, somebody has already tried to go down this path and it ended with the search for the Any.do API, as detailed by this Quora question. I'm generally not one to take "no" for an answer, so I kept digging until I found the README.md file contained in their chrome extension. This file just happens to detail the Any.do REST API that the Chrome extension uses, so I'm reposting it here with the hope that it might help somebody.

    1. Any.DO client-server REST API Documentation
    2. ===========================================
    3.  
    4. Introduction & API Notes
    5. ------------------------
    6.  
    7. Clients use REST API calls over HTTP to synchronize tasks, categories, user preferences and sharing information with the server.
    8.  
    9. There are 5 types of requests: GET, POST, PUT, DELETE, and GET with a specific ID.
    10.  
    11. Here are the supported request types:
    12.  
    13. *GET* /endpoint
    14.  
    15. *GET* /endpoint/id
    16.  
    17. *POST* /endpoint
    18.  
    19. *PUT* /endpoint/id
    20.  
    21. *DELETE* /endpoint/id
    22.  
    23. * All API is currently placed under the "/state-manager" path.
    24. * All JSON requests (all requests except the login) must have the *application/json* content type.
    25. * The API supports sending the data in dictionary mode, with flattened tasks, called "flatDict".
    26. To return the data using a dictionary with the global ID as the key of every value, use *responseType=flatDict*
    27. as a GET parameter to every request.
    28. * In JSON requests, all data sent using POST and PUT requests must be JSON encoded data in the body of the request.
    29. * All requests use a globally unique identifier encoded with URL safe base64 encoded. The globally unique ID
    30. is a 16 byte random binary string. After base64 encoding, the ID will be a 24 byte string.
    31. * in PUT, DELETE, and optionally GET requests, you need to add the 24 byte global ID to the URL.
    32.  
    33. Code to create the global ID:
    34.  
    35. function encodeSafeBase64(str) {
    36. return str.replace("+", "-").replace("/","_")
    37. }
    38.  
    39. function createGlobalId() {
    40. var randomString = "";
    41. for(var i = 0; i < 16; i++) {
    42. randomString += String.fromCharCode(Math.random() * 256);
    43. }
    44. return encodeSafeBase64(encodeBase64(randomString));
    45. }
    46.  
    47. GET requests
    48. ------------
    49.  
    50. There are two types of GET requests - with or without the ID.
    51. In flatDict mode, the response is a dictionary with all of the tasks, using their UUID as the key and a dictionary
    52. of the data as the value. If a GET request was done with the ID, the task dictionary is returned in the body as is.
    53.  
    54. POST requests
    55. -------------
    56.  
    57. To create new objects, a *list* with dictionaries of objects is sent to the server. If you need to create only one
    58. item, put a single entry in the list. Note that the global ID must be created by the client and not the server!
    59.  
    60. PUT requests
    61. ------------
    62.  
    63. To update an object, send a PUT request with the ID in the URL. The body should have the new object in the body.
    64.  
    65. DELETE requests
    66. ---------------
    67.  
    68. To delete an object, simply send a DELETE request with the ID in the URL.
    69.  
    70. Login
    71. -----
    72.  
    73. * Right now, the only form encoding call done to the server is the login process.
    74. * Only POST is supported.
    75. * The parameter *_spring_security_remember_me* should always be specified.
    76. * Password is not encoded/encrypted in the request.
    77.  
    78. Endpoint: /state-manager/j_spring_security_check
    79.  
    80. POST parameters:
    81.  
    82. {
    83. j_username: <email>,
    84. j_password: <password>,
    85. _spring_security_remember_me: "on"
    86. }
    87.  
    88. Users
    89. -----
    90.  
    91. The users table contains all user info and preferences.
    92.  
    93. Endpoint: /user
    94.  
    95. DTO:
    96.  
    97. {
    98. name: <string>,
    99. username: <email>,
    100. password: <plaintext password>,
    101. emails: [<email>],
    102. phoneNumbers: [<string>, <string>, ...],
    103. instDetails: {
    104. version_code: <int>,
    105. widget_1x4: <boolean>,
    106. widget_4x4: <boolean>,
    107. current_view: <string>,
    108. gtasks_logged_in: <boolean>,
    109. versionSDK: <int>,
    110. country: <string>,
    111. language: <string>,
    112. c2dmReceiverId: <string>,
    113. additionalData: <string>,
    114. installationId: <string>,
    115. completed_tasks: <int>,
    116. platform: <string>,
    117. todo_tasks: <int>,
    118. done_tasks: <int>,
    119. shake: <boolean>,
    120. pnsToken: <string>
    121. }
    122. }
    123.  
    124. Tasks
    125. -----
    126.  
    127. The tasks endpoint.
    128.  
    129. Endpoint: /me/tasks
    130.  
    131. DTO:
    132.  
    133. {
    134. id: <uuid>,
    135. parentGlobalTaskId: <uuid>,
    136. ownerPuid: <uuid>,
    137. title : <string>,
    138. category: <string>,
    139. creationDate: <date>,
    140. dueDate: <date>,
    141. expanded: <boolean>,
    142. priority: "Normal" / "High",
    143. status: "UNCHECKED" / "CHECKED" / "DELETED" / "DONE",
    144. repeating: <boolean>,
    145. repeatingMethod: "TASK_REPEAT_OFF" / "TASK_REPEAT_DAY" / "TASK_REPEAT_WEEK" / "TASK_REPEAT_MONTH",
    146. latitude: <string>,
    147. longitude: <string>,
    148. shared: <boolean>,
    149. subTasks: [] # not used in flat mode
    150. sharedFriends: {
    151. {
    152. puid: <uuid>,
    153. name: <string>,
    154. status: "PENDING" / "ACCEPTED" / "REJECTED" / "NEW",
    155. sharedMethod: {
    156. shareMethod: "ANYDO" / "EMAIL" / "PHONE",
    157. value: <uuid>
    158. }
    159. },
    160. ...
    161. }
    162. }
    163.  
    164. Categories
    165. ----------
    166.  
    167. The task categories endpoint.
    168.  
    169. Endpoint: /me/categories
    170.  
    171. DTO:
    172.  
    173. {
    174. id : <uuid>,
    175. name : <string>,
    176. listPosition : <int>,
    177. isDefault : <boolean>
    178. }

    To any Any.do staff that happens to see this: if you would rather that I don't repost this information, I'd ask that you post it somewhere on your site instead. What is the harm in allowing users to use the same API that your applications do? The worst that can happen is that you have many more applications that integrate with your service (on platforms that you may not want to support).

    I hope this helps somebody!

    Comments

    sean's picture
     

    sean (not verified) says:

    Hi there!

    Thanks for reverse engineering this API. I'm having some trouble authenticating, however.

    I'm POSTing this URL which returns me a 500 page with a generic HTML response:

    http://any.do/state-manager/j_spring_security_check?j_username=email%40e...

    Any thoughts?

    cweagans's picture
     

    cweagans says:

    I have no idea why it's doing that. Honestly, I haven't played with it that much. My recommendation would be to get a copy of Charles (http://www.charlesproxy.com/) and inspect the requests that the Chrome extension makes. This will make it easy to see what data needs to be passed, and how to replicate it for your own applications.

    Srinivasan Gopalan's picture
     

    Srinivasan Gopalan (not verified) says:

    Same result here. Some sort of py call trace in the 500 response.

    Seth Alves's picture
     

    Seth Alves (not verified) says:

    try Posting to

    https://sm-prod.any.do/j_spring_security_check

    with content-type application/x-www-form-urlencoded and fields:

    j_username=*login-name*
    j_password=*password*
    _spring_security_remember_me=on

    That should get you two cookies called

    JSESSIONID
    SPRING_SECURITY_REMEMBER_ME_COOKIE

    Then GETing things like:

    https://sm-prod.any.do/me
    https://sm-prod.any.do/me/categoriesresponseType=flat&includeDeleted=fal...
    https://sm-prod.any.do/me/tasksresponseType=flat&includeDeleted=false&in...

    should work.

    Yatao Li's picture
     

    Yatao Li (not verified) says:

    Great! Never thought it's written there. :-D Thanks for the information!

    ryan@reichenfeld.com's picture
     

    ryan@reichenfeld.com (not verified) says:

    Hi, I'm not a programer, but would love an any do desktop client. Dumb question, but how do I use this code to make an any do desktop client? Just plug it into terminal?

    cweagans's picture
     

    cweagans says:

    This information is really for programmers. There's unfortunately no easy way to have an Any.do desktop client based on this information, however, if you use Chrome, there is a Chrome Web App that you can install that acts very much like a desktop client. On a Mac, it's even a separate application that you can Cmd-Tab to.

    Kenny Saelen's picture
     

    Kenny Saelen (not verified) says:

    I want to do this from within C#, has anyone tried this yet. I can't seem to get past the Unauthorized exception... :|

    Kenny Saelen's picture
     

    Kenny Saelen (not verified) says:

    I am not getting past the unauthorized thing with C#.

    WebRequest req = WebRequest.Create(@"https://sm-prod.any.do/j_spring_security_check");
    req.ContentType = "application/x-www-form-urlencoded";
    req.Method = "POST";

    using (var writer = new StreamWriter(req.GetRequestStream()))
    {
    writer.Write("j_username=mymail@gmail.com");
    writer.Write("j_password=mypassword");
    writer.Write("_spring_security_remember_me=on");
    }

    HttpWebResponse resp = req.GetResponse() as HttpWebResponse;

    it ironically gives me the exception : unauthorized :)

    Bob Lamoureux's picture
     

    Bob Lamoureux (not verified) says:

    You need to first do a GET to the /me URL so that a JSESSIONID cookie gets returned

    Also, you need to create a CookieContainer so that the cookies that get returned are managed. This cookie container should then be used for all subsequent requests

    Here is a working Powershell script (as close to C# as you should need)

    Stick your username and password into the $postData line near the top

    $encoding = new-object Text.ASCIIEncoding;
    $postData = "j_username=##USERNAME-HERE##&j_password=##PASSWORD HERE##&_spring_security_remember_me=on";
    $data = $encoding.GetBytes($postData);

    $_cookieContainer = new-object Net.CookieContainer;
    [Net.HttpWebRequest]$myRequest = [Net.WebRequest]::Create("https://sm-prod.any.do/me");
    $myRequest.UserAgent = "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36";
    #$myRequest.Proxy = new WebProxy(SessionControlForm.ProxyServer);
    $myRequest.CookieContainer = $_cookieContainer;
    $myRequest.Timeout = 180000;

    $myRequest.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
    $myRequest.Headers.Add("Accept-Encoding", "gzip,deflate,sdch");
    $myRequest.Headers.Add("Accept-Language", "en-US,en;q=0.8");
    $myRequest.Headers.Add("Cache-Control", "max-age=0");

    $myRequest.Method = "GET";
    [Net.HttpWebResponse]$response = $myRequest.GetResponse()

    [Net.HttpWebRequest]$myRequest = [Net.WebRequest]::Create("https://sm-prod.any.do/j_spring_security_check");
    $myRequest.UserAgent = "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36";
    #$myRequest.Proxy = new WebProxy(SessionControlForm.ProxyServer);
    $myRequest.CookieContainer = $_cookieContainer;
    $myRequest.Timeout = 180000;

    $myRequest.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
    $myRequest.Headers.Add("Accept-Encoding", "gzip,deflate,sdch");
    $myRequest.Headers.Add("Accept-Language", "en-US,en;q=0.8");
    $myRequest.Headers.Add("Cache-Control", "max-age=0");

    $myRequest.Method = "POST";
    $myRequest.ContentType = "application/x-www-form-urlencoded";
    $myRequest.ContentLength = $data.Length;
    $newStream = $myRequest.GetRequestStream();

    $newStream.Write($data, 0, $data.Length);
    $newStream.Close();

    [Net.HttpWebResponse]$response = $myRequest.GetResponse()
    if ($response -ne $null)
    {
    }

    asyba's picture
     

    asyba (not verified) says:

    you know how i can do this in JavaScript in XMLHttpRequest and without other scripts ? the cooki part that is my problems how i send cookies in a GET request.

    Yatao Li's picture
     

    Yatao Li (not verified) says:

    Hi, following this thread, I wrote some primitive code that can go pass the authorization and pull back all the tasks.
    Not sure if I can create/update entries.

    Anyone interested in building this together?

    https://github.com/v-yadli/all.do

    Add new comment

    Filtered HTML

    • Web page addresses and e-mail addresses turn into links automatically.
    • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
    • Lines and paragraphs break automatically.

    Plain text

    • No HTML tags allowed.
    • Web page addresses and e-mail addresses turn into links automatically.
    • Lines and paragraphs break automatically.
    By submitting this form, you accept the Mollom privacy policy.