The new DoneDone: More about the new API
September 26, 2011
As mentioned in an earlier blog post, the DoneDone API is changing. We’ve just released the new implementation to everyone who has opted in to preview the new version; if you haven’t yet and would like a sneak peak at the new system and especially the new API, contact us at accounts[at]mydonedone.com, noting your current account’s subdomain and your account owner’s email address if you’d like us to set you up with an account.
Though the new API is mostly complete and has been tested internally, we’re still ironing out all of the kinks so things may not quite work perfectly for the next few days; please bear with us until we get everything finalized. With that said, this should give those that need to rewrite any of their applications that integrated with the old API the head start that they’ll need.
We’ll also be releasing a .NET client library soon for those of you on that platform.
Without further ado, here are the implementation details you need to get started.
Enable the API
You’ll need to enable the API for any projects you’d like to use with the API.

Security
To make any calls to the API, you’ll have to provide your username and password via an HTTP Basic authentication header. When the new version of DoneDone goes live on September 30th, all accounts will be on SSL so your credentials will be safe from prying eyes. Until then, you’ll be running on HTTP so you’ll want to set up a fake user account to test the API. Wikipedia will tell you everything you need to know about what the Authorization header needs to look like.
Additionally, each request you make needs to be signed with your personal API Token. This prevents someone from intercepting a request you’ve made to the API and changing the parameters of the request unless they know your secret API token, so keep it safe.
Follow these steps below to sign your requests.
- Start with the URL you are requesting exactly as you sent it – case sensitive and inclusive of query parameters. If the request you’re making is a GET request, go to step 3. Otherwise continue with step 2.
- If your request is a POST or a PUT, take the parameter keys you’re sending with your request and sort them alphabetically. Do not include the file data or keys if you’re uploading any files with your request.
- Go through this alphabetical list of keys and append the lowercased parameter name to the value sent with this parameter with no delimiters.
- Append all of these key-value pairs to the string you had at the end of step 1 (the URL you’ve requested).
- Sign your request using HMAC-SHA1 using your API token as the key and then Base64 encode this signature.
- Send this signature in an HTTP header called X-DoneDone-Signature with your request.
An example
If you were making a GET request to:
http://wearemammoth.mydonedone.com/issuetracker/api/projects
… using the username “test”, a password of “password”, and an API token 5F4CAC0C16977B21F4CE3766D913FE74, your request header should have the following headers in it:
X-DoneDone-Signature: L1Zb6/R1byxutuhgj49RMXcZgok=
Authorization: Basic dGVzdCUzQXBhc3N3b3Jk
Here’s the code we’re using in C# to calculate a signature, given a key.
All requests are made to http[s]://youraccountdomain.mydonedone.com/IssueTracker/API/MethodName/{parameters}
(currently http, but once live, all accounts will switch to https)
Methods
> Method: Get all Projects you have access to with the API enabled
URL: [GET] /Projects/{load_with_issues}
Note: The parameter load_with_issues is optional. If provided, it must be ‘true’ or ‘false’ (without single quotes). Passing true will deep load all of the projects as well as all of their active issues. It is a potentially heavy call, so be careful using it.
Example Response:
[
{
"ID":123,
"Name":"First Project",
"Issues": null
},
{
"ID":456,
"Name":"Proj 2",
"Issues": null
}
]> Method: Get all Priority Levels
URL: [GET] /PriorityLevels
Example Response:
[
{
"ID":1,
"Value":"Low"
},
{
"ID":2,
"Value":"Medium"
},
{
"ID":3,
"Value":"High"
},
{
"ID":4,
"Value":"Critical"
}
]> Method: Get all People in a Project
URL: [GET] /PeopleInProject/{project_id}
Example Response:
[
{
"ID":1,
"Value":"Ka Wai Cheung"
},
{
"ID":2,
"Value":"Mustafa Shabib"
}
]> Method: Get all Issues in a Project
URL: [GET] /IssuesInProject/{project_id}
Example Response:
[
{
"PriorityLevel":"Low",
"State":"Open",
"LastUpdatedDate":"\/Date(1314996892513)\/",
"LastViewedDate":null,
"CreateDate":"\/Date(1314996892513)\/",
"Title":"Test for API",
"OrderNumber":536,
"ProjectID":26,
"Tester":{
"ID":157,
"Value":"Tester's Name"
},
"Resolver":{
"ID":63,
"Value":"Resolver's Name"
},
"Creator":{
"ID":26,
"Value":"Ka Wai Cheung"
}
}
]> Method: Does an Issue Exist?
URL: [GET] /DoesIssueExist/{project_id}/{issue_id}
Example Response:
{
"IssueExists": true
}> Method: List of Allowed Statuses An Issue Can Transition To
URL: [GET] /PotentialStatusesForIssue/{project_id}/{issue_id}
Note: If you are an admin, you’ll get both all allowed statuses as well as ALL statuses back from the server
Example Response:
{
"PotentiallyAllowedStatusesForIssue":[
{
"ID":13,
"Value":"In progress"
},
{
"ID":14,
"Value":"Not an issue"
},
{
"ID":15,
"Value":"Not reproducible"
},
{
"ID":16,
"Value":"Missing information"
},
{
"ID":19,
"Value":"Ready for retest"
},
{
"ID":21,
"Value":"Closed"
}
],
"AllStatusesSinceYouAreAnAdmin":[
{
"ID":12,
"Value":"Open"
},
{
"ID":13,
"Value":"In progress"
},
{
"ID":14,
"Value":"Not an issue"
},
{
"ID":15,
"Value":"Not reproducible"
},
{
"ID":16,
"Value":"Missing information"
},
{
"ID":17,
"Value":"Pushed back"
},
{
"ID":18,
"Value":"Ready for next release"
},
{
"ID":19,
"Value":"Ready for retest"
},
{
"ID":20,
"Value":"Fix not confirmed"
},
{
"ID":21,
"Value":"Closed"
}
]
}> Method: Issue Details
URL: [GET] /Issue/{project_id}/{issue_id}
Note: You can use this to check if an issue exists as well, since it will return a 404 if the issue does not exist.
Example Response:
{
"Title":"Test for API",
"ProjectTitle":"First Project",
"ProjectID":123,
"OrderNumber":456,
"Description":"This is a description in MARKDOWN format.",
"PriorityLevel":"Low",
"PriorityLevelID":1,
"IssueState":"Open",
"IssueStateID":12,
"Resolver":{
"ID":3,
"Value":"Ka Wai Cheung"
},
"Tester":{
"ID":2,
"Value":"Mustafa Shabib"
},
"Creator":{
"ID":1,
"Value":"Craig Bryant"
},
"Watchers":[
{
"ID":4,
"Value":"Michael Sanders"
},
{
"ID":26,
"Value":"Ka Wai Cheung"
}
],
"Tags":[
{
"ID": 122,
"Value": "api"
}
],
"UserRoleIDs":[
3,
4,
5
],
"CompaniesAndPeopleOnIssue":[
{
"ID":1,
"Name":"We Are Mammoth",
"People":[
{
"ID":1,
"FirstName":"Mustafa",
"LastName":"Shabib",
"AccountRoleID":1,
"EmailAddress":"mustafa.shabib@wearemammoth.com"
}
]
},
{
"ID":102,
"Name":"Chicago Cubs",
"People":[
{
"ID":162,
"FirstName":"Andre",
"LastName":"Dawson",
"AccountRoleID":1,
"EmailAddress":null
}
]
}
],
"TesterResolverAndWatcherIDs":[
2,
3,
4,
26
],
"Attachments":[
{
"ID":0,
"FileUpload":"http://c3362733.r33.cf0.rackcdn.com/7a72328a-00f2-4985-80a8-6f9da3043f31_smalla.jpg",
"Bytes":162597.00,
"RelatedActionableItemHistoryID":58036,
"RelatedActionableItemHistory":null
}
],
"Histories":[
{
"ID":57406,
"AvatarURL":"http://c3363003.r3.cf0.rackcdn.com/e48bd3f6-d9be-457c-8cf3-901e99a8b8cf_kc.jpg",
"CreateDate":"\/Date(1314996892513)\/",
"Action":"Craig Bryant created the issue.",
"Description":"Assigned to *Ka Wai Cheung* as the resolver, and to *Mustafa Shabib* as the tester. This issue is marked as *Low*.",
"HistoryType":11,
"Attachments":[
]
}
]
}> Method: People that this issue can be reassigned to
URL: [GET] /PeopleForIssueAssignment/{project_id}/{issue_id}
Example Response:
[
{
"ID":162,
"Value":"Andre Dawson"
},
{
"ID":22,
"Value":"Anthony Bruno"
}
]> Method: Create an Issue
URL: [POST] /Issue/{project_id}
Post Data (parameter name, type, required or optional):
title: string, required.
description: string, optional.
tags: comma-delimited string, optional.
priority_level_id: short, required. Get a list of valid priority levels from the method documented above.
resolver_id: int, required. Get a list of people on this project from the method documented above.
tester_id: int, required. Get a list of people on this project from the method documented above.
watcher_ids: comma delimited string of people’s ids. Optional.
Files can be attached by posting the form with the content-type multipart/form-data.
Example Response:
{
"IssueID":128,
"IssueURL":"http://wearemammoth.mydonedone.com/issuetracker/projects/77/issues/128",
"SuccesfullyAttachedFiles": "true"
}> Method: Create a Comment
URL: [POST] /Comment/{project_id}/{issue_id}
Post Data (parameter name, type, required or optional):
comment: string, required.
people_to_cc_ids: comma delimited string of people’s ids. Optional.
Files can be attached by posting the form with the content-type multipart/form-data.
Example Response:
{
"CommentURL":"http://wearemammoth.mydonedone.com/issuetracker/projects/77/issues/129#history-58182",
"SuccesfullyAttachedFiles": "true"
}> Method: Updating an Issue
URL: [PUT] /issue/{project_id}/{issue_id}
Using this method you can update the tags, title, description, priority level, state (open, in progress, etc.), resolver, or tester of an issue. |
If you provide any parameters then the value you pass will be used to update the issue. If you wish to keep the value that’s already on an issue, then do not provide the parameter in your PUT data. Any value you provide, including tags, will overwrite the existing values on the issue. If you wish to retain the tags for an issue and update it by adding one new tag, then you’ll have to provide all of the existing tags as well as the new tag in your tags_list parameter, for example.
Put Data (parameter name, type):
title: string, optional.
description: string, optional.
tags: comma-delimited string, optional.
priority_level_id: short, optional. Get a list of valid priority levels from the method documented above.
state_id: short, Optional. Get a list of valid states that this issue can transition to from the method documented above.
resolver_id: int. Get a list of people on this project from the method documented above.
tester_id: int. Get a list of people on this project from the method documented above.
Example Response:
{
"IssueURL":"http://wearemammoth.mydonedone.local:64800/issuetracker/projects/77/issues/129"
}Responses
All responses will contain JSON encoded data, with the response Content-Type set to applciation/json.
Successes
200 – OK – the requested data or a success message with relevant details on POST and PUT requests will be provided as JSON encoded data described above.
Errors
Any request may respond with the following errors, each of which also provides some additional details (if any) as a simple JSON object with a message.
400 – Invalid – you are missing a required parameter or sending an invalid value for a parameter (an integer where a short was expected, for example). Check what you’re sending to the API compared to what’s expected in the documentation.
401 – Unauthorized – you need to provide correct credentials via the basic authentication header
403 – Forbidden – you’re authenticated, but you aren’t authorized to perform the request
404 – Not Found – We can’t find whatever you asked for (bad method, no issue found, no project found, etc).
409 – Conflict – You’ve made too many requests to the same resource. You can only request the same URL from the same IP address once every five seconds. We may adjust this rate limit in the future.
500 – Internal error – we did something wrong!
Example Error Response
Here’s an example 404 response when attempting to update an issue that does not exist.
< HTTP/1.1 404 Not Found
< Server: ASP.NET Development Server/9.0.0.0
< Date: Mon, 26 Sep 2011 18:38:14 GMT
< Cache-Control: private
< Content-Type: application/json; charset=utf-8
< Content-Length: 26
< Connection: Close
<
{ "Message": "Not found."}Wrap up
Let us know if you’ve run into any issues or have any additional ideas for useful methods. Now that we’ve made the switch to the new API system, adding and modifying the API will be much easier and so we can continue to improve it as everyone contributes ideas. Additionally, if you run into any issues trying to use this beta release of the API, please let us know by writing to me directly at mustafa.shabib[at]wearemammoth.com.

Just a thought, but if all requests are going over https, isn’t it a bit redundant to require a signed signature? I think signatures can be frustrating for developers, so if it’s not really needed, maybe you should drop requiring it.
Also, is your example accurate? If I base64 encode an hmac sha1 using http://wearemammoth.mydonedone.com/issuetracker/api/projects as the data to sign (no extra params appended because it’s a GET request) and 5F4CAC0C16977B21F4CE3766D913FE74 as the key, the output I’m seeing is L1Zb6/R1byxutuhgj49RMXcZgok=
Thanks!
Hey Rob –
Thanks for the comments. You’re absolutely right – thanks for catching the mistake in the post re: the example hash! Really sorry about the confusion. I’ve updated the post above with the correct value, which matches what you’re getting.
In the spirit of transparency, let me explain why we opted to require signatures on incoming requests from API users.
As far as the signature requirement being redundant or frustrating, I’m not in disagreement with you. The reason we opted to make it required was because when we started development, we hadn’t decided if all accounts would be running over SSL. While we will initially be rolling the new version out to all users with SSL, we may someday have a plan that may not run on SSL and rather than create a set of rules to use the API that further complicated things (as in: if you’re using SSL, you don’t need to sign your messages, if you aren’t using SSL then you are required to sign your requests), we opted to make it a requirement from the beginning, both to give us the most leeway and to prevent further changes to the API requirements for client library developers.
To that point – with client libraries that we’re hoping will soon start appearing (we’ll release a .NET library soon after the new DoneDone’s release), the signing of messages will be transparent to most developers using the API. Hopefully, this cuts down on the frustration.
So, mostly, it’s a throwback to a time we weren’t sure about the SSL options we’d be providing to everyone and additionally it provides us with the most flexibility to offer as many people as possible access to the API in a secure manner regardless of which plan they’re on in the present or in the future without requiring anyone to change the code they’re using to integrate with the new API.
Thanks for writing.
Mustafa
Thanks, Mustafa. I just implemented the api in php which I can open source on github at some point soon. Everything works fine so far although I’ve only needed to implement GET requests. The IssuesInProject endpoint is giving me an error that it can only be requested every 5 seconds — even if I’m requesting issues from a different project. I think that is an oversight. I currently have to put sleep(5) statements in my script that pulls issues into our mashboard which makes it quite slow to update (we do cache it for an hour, but still). Thanks!
Hey Rob
Sorry for the delay in getting back to you.
That’s great to hear that you’ve finished up the API wrapper for PHP – would love to link back to it from our site once you publish it on github. Drop us a line when you think it’s ready.
Ahhh…rate limiting. I confess that I knew going in that we may have been a little strict with the way it’s been implemented. Currently – the API uses a combination of the API method you are calling on your domain name (such as mycompany.wearemammoth.com/issuetracker/api/projects) and your requesting IP address to determine whether you’re allowed to call the method. As you noticed, it only permits you to call the same method from your IP address once every five seconds.
As mentioned in a previous post about the API – I’m open to changing this limit or how it’s implemented as we get a feel for how it’s being used. I’m not sure if using the parameters passed in to a method, however, is enough to protect our side from being flooded with requests. If we rate limited method calls with their incoming parameters originating from the same IP address, we open ourselves up to a situation where someone could call mycompany.mydonedone.com/issuetracker/api/peopleinproject/[ANY_NUMBER] however many times they like within any period of time, thus essentially bypassing the ratelimiting.
With all that being said, I also understand that this seems unnecessarily paranoid and I’d love to hear any ideas you may have as to how you think the rate limit should be implemented. Feel free to email me directly at mustafa.shabib[at]wearemammoth.com for what I promise will be a much faster response.
Thanks again for writing.
Mustafa
Sorry for being obtuse, but what kind of date format is used in LastUpdatedDate etc (and any hint how a php only guy would convert this to a unix timestamp) ?
Thany you !
–eike
Hi Eike -
Dates should be returned as the number of milliseconds since Unix epoch time. Divide by a thousand and you should be able to use the PHP *date* function (http://php.net/manual/pt_BR/function.date.php) to create the date in any format you choose – just remember to divide the value we pass back to you by 1000 before doing so.
Mustafa
Hi Mustafa,
thank you for your help!
- eike
No problem Eike!