Redmine API at Planio
Planio is built on top of Redmine, an awesome open source project management software. Hence, the Planio API is 100% compatible to the Redmine REST API with a few additions and enhancements.
The Planio API is using REST (Representational state transfer) and provides the basic CRUD operations being Create, Read, Update, Delete via HTTPS.
As a representation format, the Redmine API (and hence Planio’s REST API) supports JSON and XML. In the following examples, we’ll sometimes use XML and sometimes JSON. However, all API operations support both formats.
In order for the REST API to work, please navigate to Your Avatar → Administration → Settings → Authentication and check the box next to Enable REST API.
Authentication
While some parts of the API can be used without authentication – depending on how you have configured your Planio account – most actions will require requests to be authenticated.
OAuth 2.0
In order to give an application permission to access certain parts of your data, Planio uses the OAuth 2.0 protocol. Access granted via OAuth 2 is limited to a certain set of Redmine permissions. For example it is possible to build a third party application that only can create issues on behalf of those users that have authorized it, but nothing else.
Of the various grant types defined by the OAuth 2.0 specification, Planio currently implements the most commonly used Authorization Code Grant type. Refresh Tokens as a way of obtaining a new access token when the current access token becomes invalid or expires are supported as well.
At this time we support only confidential applications, where the client secret can be kept reasonably secure. Contact us if you are uncertain of the type of your application, or if you know you wish to implement a public application such as a browser-based or native application.
Registering an Application
Setting up the OAuth 2 client
require 'oauth2'
client_id = '...' # application uid
client_secret = '...' # application secret
redirect_uri = '...' # redirect uri
site = "https://your-account.plan.io/" # your planio account's URL
client = OAuth2::Client.new(client_id, client_secret, site: site)
In order to use OAuth 2, first of all make sure the REST API is enabled (see above). Then, as an Administrator, navigate to Your Avatar → Administration → Settings → Applications and click New Application. Choose a descriptive name, enter one or more redirect URIs (these depend on the application in question) and select the permissions this application should be granted. When requesting authorization from a user, an application can choose to request any subset of the permissions you selected here.
A note about the admin
scope. Selecting this scope for an application means that, when authorized by an administrator, the application will be able to do everything that is possible via the API - just as if you were using an admin’s API key, or signing in as an admin through the web interface. In most cases this admin super power will not be necessary and should therefore be avoided, but the option is there if you need it.
After saving the application, take note of the generated Application UID and Secret, which are needed for setting up the OAuth 2 client. The example on the right shows how you would do this with the OAuth 2 Ruby library
Requesting Authorization from a User
Redirect the user to the authorization URL for your application:
state = SecureRandom.hex
session[:state] = state # store for later comparison
url = client.auth_code.authorize_url(redirect_uri: redirect_uri,
scope: 'add_issues',
state: state)
# => "https://your-account.plan.io/oauth/authorize?response_type=code&..."
redirect_to url # Assuming this is a Rails controller, you would do this.
The authorization endpoint is https://your-account.plan.io/oauth/authorize
. It accepts the following parameters:
Parameter | Description |
---|---|
response_type | must be code |
client_id | your application’s UID |
redirect_uri | URI you expect the client to be redirected to after successful authorization. Must match one of the redirect URIs configured earlier |
scope | list of the permissions requested. Separate multiple permissions with spaces |
state | optional parameter for CSRF protection. Will be passed back to redirect_uri |
Please refer to the Authorization Request section of the OAuth 2.0 specification for more information.
List of valid scopes
The scopes in our OAuth implementation map directly to Planio permissions. In addition to that, there is the admin
scope, which maps to a user’s Administrator status. What an authorized application can actually do in any given context is determined by the intersection of granted scopes and the authorizing users’ permissions in this context.
Feature | Available Scopes |
---|---|
Project | add_project edit_project close_project select_project_modules manage_members manage_versions add_subprojects manage_public_queries save_queries |
Agile | manage_public_agile_queries manage_agile_verions add_agile_queries view_agile_queries view_agile_charts |
Forums | view_messages add_messages edit_messages edit_own_messages delete_messages delete_own_messages manage_boards |
Calendar | view_calendar |
Team Chat | chat view_chat_log_entries delete_chat_logs manage_chat_settings |
Help Desk | manage_customers view_customers view_crm_templates manage_crm_templates email_customers manage_crm_settings view_crm_faqs |
Documents | view_documents add_documents edit_documents delete_documents |
Files | view_files manage_files |
Gantt | view_gantt |
Issue tracking | view_issues add_issues edit_issues copy_issues manage_issue_relations manage_subtasks set_issues_private set_own_issues_private add_issue_notes edit_issue_notes edit_own_issue_notes view_private_notes set_notes_private delete_issues view_issue_watchers add_issue_watchers delete_issue_watchers import_issues manage_categories add_to_attachments_blacklist view_checklists done_checklists edit_checklists manage_checklist_templates |
Meet | join_video_meeting |
Blog | view_news manage_news comment_news |
Pidoco | manage_pidoco |
Repository | view_changesets browse_repository commit_access manage_related_issues manage_repository |
Resources | view_resources add_booking edit_booking |
Storage | view_storage_nodes manage_storage_nodes view_protected_storage_nodes manage_protected_storage_nodes manage_shared_storage_files view_shared_storage_files view_storage_file_downloads |
Time tracking | view_time_entries log_time edit_time_entries edit_own_time_entries manage_project_activities |
Wiki | view_wiki_pages view_wiki_edits export_wiki_pages edit_wiki_pages rename_wiki_pages delete_wiki_pages delete_wiki_pages_attachments protect_wiki_pages manage_wiki |
Getting an Access Token
Handle the callback to your redirect URI and exchange the authorization code for an access token
# assuming params is your hash of URL parameters
unless params[:state] == session[:state]
fail "invalid state parameter"
end
code = params[:code]
token = client.auth_code.get_token(code, redirect_uri: redirect_uri)
# => <#OAuth2::AccessToken ...>
A successful authorization will end up in a redirect to redirect_uri
with a code
parameter holding the authorization code. If you passed a state
parameter earlier, it will be present in the response as well and the two values have to match.
In order to do something useful with the API, the authorization code has to be exchanged for an access token. This access token can then be used like an API key, but will be restricted to the permissions requested earlier with the scope
parameter, and it will only be valid for a limited time.
The access token endpoint is https://your-account.plan.io/oauth/token
. It accepts the following parameters in a POST request:
Parameter | Description |
---|---|
grant_type | must be authorization_code |
code | authorization code received earlier |
redirect_uri | the same redirect_uri value that was used earlier |
To authenticate your application, use HTTP Basic Authentication with this request, using your application’s uid as username and the secret as password.
Alternatively, you can also specify the application’s uid and secret in the request body using the client_id
and client_secret
parameters.
Making Authorized Requests
Use the access token to make authorized requests
# get the actual token string from the oauth lib
access_token = token.token
# compile issue data
payload = { issue: { subject: "Hello world" } }
# specify the token in the Authorization HTTP header
headers = { Authorization: "Bearer #{access_token}"}
RestClient.post "https://your-account.plan.io/projects/some-project/issues.json",
payload,
headers
# => <RestClient::Response 201 "{\"issue\":{\"...">
Planio’s access tokens are Bearer Tokens, which simply means that the token value is to be sent with each request, in an Authorization
header like that:
Authorization: Bearer actual-token-value
Using Refresh Tokens
Check and refresh an expired access token
token.expired?
# => true
new_token = token.refresh!
Access tokens issued by Planio will expire after a set amount of time. For retrieval of a fresh access token without further user interaction, a Refresh Token is issued together with each access token. This refresh token can be exchanged for a new access token.
As before, when getting the initial access token, the new access token is retrieved by issuing a POST request to the access token endpoint https://your-account.plan.io/oauth/token
. This time, instead of a code
, pass the refresh token as refresh_token
parameter, and refresh_token
as value for the grant_type
parameter.
Refresh tokens can only be used once, and since they never expire, special care has to be taken in order to keep them confidential.
Further Reading
- OAuth.net
- The OAuth 2.0 Authorization Framework
- The OAuth 2.0 Authorization Framework: Bearer Token Usage
- OAuth 2.0 Threat Model and Security Considerations
Deprecated authentication methods
API Keys
To get information about your user record using API authentication you could use this code:
curl 'https://example.plan.io/users/current.json' \
-H 'X-Redmine-API-Key: 0123456789abcdef'
You would get JSON like this in return:
{
"user": {
"id": 123,
"login": "john.doe@example.com",
"firstname": "John",
"lastname": "Doe",
"name": "John Doe",
"mail": "john.doe@example.com",
"created_on": "2007-10-06T13:47:36Z",
"last_login_on": "2016-06-17T18:34:34Z",
"api_key": "0123456789abcdef",
"status": 1,
"custom_fields": [{
"id": 1,
"name": "Work Phone",
"value": "+1234567890"
}]
}
}
As an alternative to using OAuth 2.0, the Redmine API by Planio still supports traditional API authentication via user specific API keys.
Using an API key, your app will get full access to Planio according to the permissions assigned to the user whose API key is used.
To use API key authentication, please make sure that the REST API is active, then navigate to your avatar → My account, open the sidebar and click on Show below the API Access Key header. After confirming your password, Planio will show your API key.
Now, to authenticate API requests, please add a custom HTTP header called X-Redmine-API-Key
and supply your API key as a value.
Other authentication methods
For compatibility with the Redmine API, the Planio REST API also supports the following deprecated ways of authentication:
You can use your Planio/Redmine API key and supply it using either
- as the username part of HTTP Basic Authentication with a random or empty password, or
- as the value of a query string parameter called
key
(not recommended, because it may be stored as part of the URL in all sorts of log files).
You can use your regular Planio login (usually your email address) and your password and supply them using HTTP Basic Authentication.
User Impersonation
The following request would impersonate Jane Schmoe:
curl 'https://example.plan.io/users/current.json' \
-H 'X-Redmine-API-Key: 0123456789abcdef' \
-H 'X-Redmine-Switch-User: jane.schmoe@example.com'
As an administrator, you can use the API in order to submit requests assuming the identity of another user. In order to do that, please specify the user’s login as a value for the X-Redmine-Switch-User
header.
This also works via OAuth, provided your application has the admin
scope and the authorizing user is actually an administrator.
Content type
To change the subject of issue #1 using JSON you could use this:
curl 'https://example.plan.io/issues/1.json' \
-X PUT \
-H 'X-Redmine-API-Key: 0123456789abcdef' \
-H 'Content-Type: application/json' \
-d '{ "issue": { "subject": "New subject" } }'
The same request using XML would look like this:
curl 'https://example.plan.io/issues/1.xml' \
-X PUT \
-H 'X-Redmine-API-Key: 0123456789abcdef' \
-H 'Content-Type: application/xml' \
-d '<issue><subject>New subject</subject></issue>'
When working with resources using the Redmine API at Planio, please specify the content type you are expecting to receive/send by using either the .json
or .xml
suffix in the URL.
When submitting content via POST
or PUT
, you will also have to specify the format by setting the Content-Type
header.
- When submitting JSON, please use
Content-Type: application/json
as a header. - When submitting XML, please use
Content-Type: application/xml
as a header.
Collections and Pagination
This will return the first 50 issues:
curl 'https://example.plan.io/projects.json?limit=50' \
-H 'X-Redmine-API-Key: 0123456789abcdef'
To get 10 users starting at the 21st you’d use:
curl 'https://example.plan.io/users.xml?limit=10&offset=20' \
-H 'X-Redmine-API-Key: 0123456789abcdef'
By default, when querying collection resources (e.g. /projects.json
), you will only receive the first 25 objects in the collection.
To receive more objects you can use the following query parameters:
Parameter | Default | Description |
---|---|---|
limit | 25 | The number of objects to return in the response, maximum is 100 |
offset | 0 | The index of the first element to return |
Associations
To get issue #123 including its history (i.e.
journals
) and repository commits (i.e.changesets
):
curl 'https://example.plan.io/issues/123.xml?include=journals,changesets' \
-H 'X-Redmine-API-Key: 0123456789abcdef'
Would return something like this:
<issue>
<id>123</id>
<subject>This is a bug</subject>
<description>Please fix it</description>
<!-- ... -->
<changesets type="array">
<changeset revision="42">
<user id="9" name="John Doe"/>
<comments>This fixes #123 @1h</comments>
<committed_on>2016-01-09T09:50:31Z</committed_on>
</changeset>
</changesets>
<journals type="array">
<journal id="123">
<user id="9" name="John Doe"/>
<notes>Status applied in changeset r42.</notes>
<created_on>2016-01-09T09:50:31Z</created_on>
<details type="array">
<detail property="attr" name="status_id">
<old_value>1</old_value>
<new_value>3</new_value>
</detail>
</details>
</journal>
</journals>
</issue>
To save requests, you can specify the associated objects you wish to receive included in your response using the include
query parameter.
Parameter | Default | Description |
---|---|---|
include | — | A comma-separated list of associations to include |
Custom fields
Fetching an issue will include its custom fields:
curl 'https://example.plan.io/issues/123.json' \
-H 'X-Redmine-API-Key: 0123456789abcdef'
You would get JSON like this in return:
{
"issue": {
"id": 123,
"subject": "A sample issue",
"custom_fields": [{
"id": 42,
"name": "Department",
"value": "Marketing"
}]
}
}
To update issue
123
and setDepartment
toSales
:
curl 'https://example.plan.io/issues/123.json' \
-X PUT \
-H 'X-Redmine-API-Key: 0123456789abcdef' \
-H 'Content-Type: application/json' \
-d@- <<EOF
{
"issue": {
"custom_fields" : [
{ "id": 42, "value": "Sales" }
]
}
}
EOF
The same request written in XML would look like this:
curl 'https://example.plan.io/issues/123.xml' \
-X PUT \
-H 'X-Redmine-API-Key: 0123456789abcdef' \
-H 'Content-Type: application/xml' \
-d@- <<EOF
<issue>
<custom_fields type="array">
<custom_field id="42">
<value>Sales</value>
</custom_field>
</custom_fields>
</issue>
EOF
One of the strengths of Redmine (and thus Planio) is its customizability. You can define custom fields for most objects in Planio and set/get values via the user interface as well as via the REST API.
No additional URL parameters are required to get/set custom fields. The custom fields array will contain a number of custom field objects with the following attributes:
Attribute | Required | Description |
---|---|---|
id | yes | The internal id of this custom field |
value | yes | The value of the custom field for this object, can be an array if multiple is set to true |
name | no | The name of the custom field |
multiple | no | Is true if the custom field allows multiple values to be set |
To set custom fields in a POST
or PUT
request, be sure to include the custom field id
with the request.
The custom field name
is supplied when fetching objects via GET
, but is’s not required when changing custom field values.
File attachments
Fetching an issue including all attachments:
curl 'https://example.plan.io/issues/123.xml?include=attachments' \
-H 'X-Redmine-API-Key: 0123456789abcdef'
You would get XML like this in return:
<issue>
<id>123</id>
<subject>An issue with an attachment</subject>
<description>This issue has a file attached to it.</description>
<!-- ... -->
<attachments type="array">
<attachment>
<id>41</id>
<filename>example.png</filename>
<filesize>13832</filesize>
<content_type>image/png</content_type>
<description>A sample image</description>
<content_url>https://example.plan.io/attachments/download/41/example.png</content_url>
<author id="123" name="John Doe"/>
<created_on>2016-06-20T12:22:10Z</created_on>
</attachment>
</attachments>
</issue>
Some objects (e.g. issues, documents, wiki pages,…) support file attachments. They can be fetched like all other associations by setting the include=attachments
query parameter. The attachments array will contain a number of attachment objects with the following attributes:
Attribute | Description |
---|---|
id | The internal id of this attachment |
filename | The filename of this attachment |
filesize | The size of this attachment in bytes |
content_type | The content type of this attachment, e.g. image/png |
description | The desription of this attachment |
content_url | The URL at which the attachment can be downloaded |
author | The user who has uploaded the attachment, identified by its id and name |
created_on | The date and time at which the attachment was uploaded |
Attaching files to new or existing objects is a two-step process:
- Upload the file, receive a token
- Create/Update the object, supply the token to attach the file
Uploading a file
To upload the local file
example.png
, issue a request like this:
curl 'https://example.plan.io/uploads.json' \
-X POST \
-H 'X-Redmine-API-Key: 0123456789abcdef' \
-H 'Content-Type: application/octet-stream' \
--data-binary @example.png
You will receive an id and a token like this:
{ "upload": { "id": 42, "token": "42.27774ef4d1a1bb92eb305e0b4526767c" } }
POST /uploads.[json|xml]
Simply issue a POST
request to while setting the Content-Type
header to application/octet-stream
and include the file content in the request body.
The response will include the token for your uploaded file.
Attaching a file
To create a new issue and attach the previously uploaded image:
curl 'https://example.plan.io/projects/my-project/issues.json' \
-X POST \
-H 'X-Redmine-API-Key: 0123456789abcdef' \
-H 'Content-Type: application/json' \
-d@- <<EOF
{
"issue": {
"subject": "A new issue with an image",
"uploads" : [
{
"token": "42.27774ef4d1a1bb92eb305e0b4526767c",
"filename": "example.png",
"content_type": "image/png",
"description": "A sample image"
}
]
}
}
EOF
Now, you can create or update an object while supplying the token and file metadata as follows:
Attribute | Required | Description |
---|---|---|
token | yes | The token received in the previous step |
filename | no | The filename for the file attachment |
content_type | no | A content type which should be used to interpret the content, e.g. image/png |
description | no | A short description to be displayed next to the filename |
Planio Storage
Planio Storage organizes files in folders, just like on your local file system. Users can use a Storage client app to synchronize their devices, or use the web interface to upload and view files.
Every project in which Storage is enabled has a single root folder with a name that equals its project identifier
.
WebDAV support
In addition to the API methods for Storage files and Storage folders outlined below, files and folders in Planio Storage can also be retrieved and manipulated using WebDAV. The top level directory which is located at https://example.plan.io/dav/
holds a folder for each project in your account that has Planio Storage enabled.
The contents of these folders can be read and written using standard WebDAV clients and client libraries. Please note that the project folders themselves cannot be renamed, moved or deleted.
Listing folder contents
A sample folder listing in JSON format
{
"storage_folder": {
"id": 1,
"name": "my_project",
"created_on": "2019-01-19T08:07:34Z",
"creator": { "id": 5, "name": "John Doe" },
"updated_on": "2019-01-19T08:07:34Z",
"updater": { "id": 5, "name": "John Doe" },
"type": "storage_folder",
"webdav_url": "https://example.plan.io/dav/my_project",
"location":"https://example.plan.io/projects/my_project/storage/folders/1.json"
"children":[
{
"id":2,
"name":"test 1",
"created_on":"2019-01-19T08:00:05Z",
"creator": { "id":1, "name":"Administrator" },
"updated_on":"2019-01-26T01:22:42Z",
"updater": { "id":1, "name":"Administrator" },
"webdav_url":"https://example.plan.io/dav/my_project/test 1",
"parent": { "id":1 },
"type":"storage_folder",
"location":"https://example.plan.io/projects/my_project/storage/folders/2.json"
},
{ "id":19,
"name":"photo.jpg",
"created_on":"2019-01-19T08:00:17Z",
"creator": { "id":1, "name":"Administrator" },
"updated_on":"2019-01-26T01:22:38Z",
"updater": { "id":1, "name":"Administrator" },
"webdav_url":"https://example.plan.io/dav/my_project/photo.jpg",
"parent": { "id":1 },
"filesize":1360573,
"content_type":"image/jpeg",
"type":"storage_file",
"location":"https://example.plan.io/projects/my_project/storage/files/19.json",
"download_url":"https://example.plan.io/projects/my_project/storage/files/19/download/photo.jpg"
}
]
}}
Listing the root folder of a project
To retrieve a project’s root folder and information about it’s contents, issue a GET
request to the storage
endpoint of the project:
GET /projects/<identifier>/storage.json?include=children
Each file or folder has a location attribute holding the URL which you can issue further GET requests to in order to retrieve this resources details / children or, using other HTTP verbs, to manipulate it.
Files and folders in a folder listing can be told apart by looking at their type attribute. For files, also the file size and content type are given.
Listing sub folder contents
Folder information below the root level is retrieved using GET requests to the storage/folders
endpoint.
To retrieve information about the contents of a folder, again add the include=children
query parameter:
GET /projects/<identifier>/storage/folders/<id>.json?include=children
Validation and Errors
Creating a project without supplying a name like this:
curl 'https://example.plan.io/projects.json' \
-X POST \
-H 'X-Redmine-API-Key: 0123456789abcdef' \
-H 'Content-Type: application/json' \
-d '{ "project": { "description": "This is my project" }}'
Would yield a
422 Unprocessable Entity
error response like this:
{
"errors": [
"Name can't be blank",
"Identifier can't be blank"
]
}
Most objects in Planio apply constraints to the values being set for its attributes. If an object cannot be created or updated due to any of the constraints not being met, you will receive an HTTP response with status 422 Unprocessable Entity
and a body detailing the problem.
Resources
This section provides detailed documentation on all resources available via the Redmine API at Planio.
Issues
A sample issue in XML format:
<issue>
<id>123</id>
<project id="1" name="Mission to Mars"/>
<tracker id="2" name="Support"/>
<status id="1" name="New"/>
<priority id="4" name="Normal"/>
<author id="5" name="John Doe"/>
<category id="126" name="Maintenance"/>
<fixed_version id="231" name="Takeoff"/>
<subject>Rocket booster firmware upgrade</subject>
<description>
The current firmware for the rocket boosters
needs to be updated.
</description>
<start_date/>
<due_date/>
<done_ratio>0</done_ratio>
<is_private>false</is_private>
<estimated_hours>30.0</estimated_hours>
<spent_hours>0.0</spent_hours>
<total_spent_hours>0.0</total_spent_hours>
<reply_token>abcdef</reply_token>
<tracking_uri>
https://example.plan.io/track/123/abcdef
</tracking_uri>
<created_on>2016-07-29T10:05:06Z</created_on>
<updated_on>2016-07-29T10:05:11Z</updated_on>
<closed_on/>
</issue>
Access issues in your Planio account via this endpoint.
Issue representations can have the following attributes:
Attribute | Required | Read-only | Description |
---|---|---|---|
id | no | yes | Internal identifier of this issue |
project | yes | no | Project to which the issue belongs, identified by its id and name ; see Projects |
subject | yes | no | Issue subject |
tracker | no | no | Tracker to which the issue belongs, identified by its id and name ; see Trackers |
status | no | no | Issue status, identified by its id and name ; see Issue statuses |
priority | no | no | Priority, identified by its id and name ; see Issue priorities |
author | no | yes | User who created the issue, identified by their id and name ; see Users |
category | no | no | Issue category, identified by its id and name ; see Issue categories |
fixed_version | no | no | Sprint/Milestone to which the issue belongs, identified by its id and name ; see Sprints/Milestones |
assigned_to_id | no | no | User to which the issue is assigned, identified by its id and name ; see Users |
parent | no | no | Issue of which this issue is a child issue, identified by its id |
watchers | no | no | Array of users who are watching this issue, identified by their id and name ; see Users |
custom_fields | no | no | see custom fields |
description | no | no | Issue description |
start_date | no | no | Date on which the issue starts; formatted as YYYY-MM-DD |
due_date | no | no | Date on which the issue is due; formatted as YYYY-MM-DD |
done_ratio | no | no | % done percentage; an integer from 0-100 |
is_private | no | no | Whether or not this issue is marked private; boolean |
estimated_hours | no | no | Estimated time in hours; specify as float or HH:MM |
spent_hours | no | yes | Total time tracked on this issue |
total_spent_hours | no | yes | Total time tracked on this issue, including any sub issues |
reply_token | no | yes | A secret token required to update this issue via email when using Planio Help Desk |
tracking_uri | no | yes | A secret URI which can be used to view those parts of the issue that are visible to the customer when using Planio Help Desk |
created_on | no | yes | Date and time at which the issue was created; formatted as ISO 8601 timestamp |
updated_on | no | yes | Date and time at which the issue was last updated; formatted as ISO 8601 timestamp |
closed_on | no | yes | Date and time at which the issue was last set to a closed status; formatted as ISO 8601 timestamp |
The attributes reply_token
and tracking_uri
are Planio additions which are not present in the standard Redmine API.
Listing issues
GET /issues.[json|xml]
A GET
request will return a paginated list of issues.
Possible parameters:
Parameter | Default | Description |
---|---|---|
sort | — | Specify an attribute you’d like to sort by; append :desc for reverse the sort order |
project_id | — | Filter by project; use a numeric id here |
subproject_id | — | Filter by subproject; if you use project_id=123&subproject_id=!* you will receive only issues of project 123 and not issues of any of its subprojects |
tracker_id | — | Filter by tracker; use a numeric id here |
status_id | open | Filter by status; you can use open , closed , * , or a numeric status id here |
assigned_to_id | — | Filter by assignee; use a numeric user id here, or me to get issues assigned to the authenticated user |
created_on | — | Filter by date created; dates should be represented as ISO 8601; you can use additional operators <= , >= here |
updated_on | — | Filter by date last updated; dates should be represented as ISO 8601; you can use additional operators <= , >= here |
closed_on | — | Filter by date last closed; dates should be represented as ISO 8601; you can use additional operators <= , >= here |
cf_$FIELD | — | Filter by custom field value; $FIELD must be a custom field id and the custom field must be defined as used as filter |
include | — | Using relations as a value will return related issues as well |
limit | 25 | The number of objects to return in the response, maximum is 100 |
offset | 0 | The index of the first element to return |
Showing an issue
GET /issues/123.[json|xml]
A GET
request will return a representation of a issue 123
. The representation will be formatted as shown above.
The request can take a single parameter called include
with one or more of these values, comma-separated:
Parameter | Description |
---|---|
children | Sub-issues that have the current issue as a parent |
attachments | File attachments attached to the issue |
relations | Issues related to this issue (duplicates, following, blocking, etc.) |
changesets | Repository changesets associated with the issue |
journals | History of changes made to this issue in the past |
watchers | Users who are currently watching this issue |
Creating an issue
POST /issues.[json|xml]
A POST
request will create a new issue. The representation must be formatted as shown above. All parameters which are not marked as read-only can be set during a create request.
Associated objects have to be specified using the parameter name with an _id
suffix, e.g. project_id
.
Watchers can be set by supplying an array of user ids as a value for the watcher_user_ids
parameter.
Issues (via contact forms)
Using Planio Help Desk, issues can be created in a simplified way, without authentication in third party web apps or via plain HTML web forms. To learn more, read the full guide on online forms with Planio.
Creating an issue via a web app
To create a new contact form issue:
curl 'https://example.plan.io/helpdesk.json' \
-X POST \
-H 'Content-Type: application/json' \
-d@- <<EOF
{
"name": "Carl Customer",
"mail": "carl.customer@example.com",
"description": "How can I...?",
"project": "support-project"
}
EOF
POST /helpdesk.[json|xml]
The /helpdesk
endpoint will accept unauthenticated requests using JSON or XML, so you can create contact form issues via your dynamic web site or third party application. It will create a new issue when Planio Help Desk is activated. Since the request will not need any authentication, it is perfect for contact forms.
Requests can include the following attributes:
Attribute | Required | Description |
---|---|---|
project | yes | Project in which the issue should be created, identified by its alpha-numric identifier; Planio Help Desk has to be enabled |
yes | Email address of the customer opening the issue; will be used to match or create company and contact | |
name | no | Name of the customer opening the issue |
subject | no | Issue subject; optional if description is given |
description | no | Issue description; optional if subject is given |
category | no | Issue category; must match existing issue category name in the given project |
custom_field_values[issue][$ID] | no | Value for an issue custom field; $ID has to be the numeric id of the custom field |
custom_field_values[company][$ID] | no | Value for a company custom field; $ID has to be the numeric id of the custom field |
custom_field_values[contact][$ID] | no | Value for a contact custom field; $ID has to be the numeric id of the custom field |
The response would look like this:
Location: https://example.plan.io/track/1234/0123456789abcdef
{
"issue": {
"id": 1234,
"reply_token": "0123456789abcdef",
"tracking_uri": "https://example.plan.io/track/1234/0123456789abcdef"
}
}
The response will include a reduced issue representation with the following attributes:
Attribute | Required | Read-only | Description |
---|---|---|---|
id | no | yes | Internal identifier of this issue |
reply_token | no | yes | A secret token required to update this issue via email when using Planio Help Desk |
tracking_uri | no | yes | A secret URI which can be used to view those parts of the issue that are visible to the customer when using Planio Help Desk |
Creating an issue using an HTML form
The following HTML code could be embedded into any web site:
<!-- CSS to hide honeypot field -->
<style media="screen">#url { display:none; }</style>
<form action="https://example.plan.io/helpdesk" method="POST" accept-charset="UTF-8">
<input name="utf8" type="hidden" value="✓" />
<label for="name">Your name:</label>
<input name="name" id="name" type="text" />
<label for="mail">Your email address:</label>
<input name="mail" id="mail" type="email" />
<label for="customer-no">Your Customer Number:</label>
<input name="custom_field_values[company][123]" id="customer-no" type="text" />
<label for="category">Category:</label>
<select name="category" id="category">
<option value="Sales">Sales</option>
<option value="Support">Support</option>
</select>
<label for="subject">Subject:</label>
<input name="subject" id="subject" type="text" />
<label for="description">Your message:</label>
<textarea name="description" id="description"></textarea>
<input name="project" type="hidden" value="example-project" />
<input name="url" id="url" type="text" />
<input type="submit" value="Submit request" />
</form>
POST /helpdesk
If you do not want to implement any server-side code at all, Planio will also accept an unauthenticated POST
request with application/x-www-form-urlencoded
form data.
This can be used to implement a contact form on an external web site which will create issues in Planio for each request. The form can include the same attributes as shown above.
If you include a field called category
, Planio will try to find an existing issue category by that name and set it on the issue. If your categories are configured with an Assignee, new issues created via this API will be assigned accordingly.
You can include an extra form field called url
as a Honeypot Captcha against SPAM. If a value is given for this attribute, the issue will not be created. To make this strategy work, the field should be hidden using CSS or JavaScript.
The request will return a 301
response with a Location
header set to the URL specified in the request’s Referer
header, resulting in a redirect back to the originating URL.
Projects
A sample project in XML format:
<project>
<id>12</id>
<name>Website Redesign</name>
<identifier>website-redesign</identifier>
<description>
We need a new website!
</description>
<created_on>2020-03-02T10:31:47Z</created_on>
<updated_on>2020-03-02T10:31:47Z</updated_on>
<is_public>false</is_public>
</project>
Projects have the following attributes:
Attribute | Required | Read-only | Description |
---|---|---|---|
id | no | yes | Internal id of this project |
name | yes | no | Name of the project |
identifier | yes | yes | Unique, immutable project identifier. Can be used in place of the project ID in request URLs. |
description | no | no | Textual description of the project. Wiki syntax is available here. |
custom_fields | no | no | see custom fields |
created_on | no | yes | Date and time at which the project was created; formatted as ISO 8601 timestamp |
updated_on | no | yes | Date and time at which the project was last updated; formatted as ISO 8601 timestamp |
Listing projects
GET /projects.[json|xml]
A GET
request will return a paginated list of projects. The include
query parameter can be used to enrich the returned data with the following information (combine multiple values with ,
):
Value | Description |
---|---|
trackers | Trackers configured for the project |
issue_categories | Issue categories available in this project |
enabled_modules | Enabled modules of the project |
time_entry_activities | Activities available for time entries in this project |
issue_custom_fields | List of issue custom fields that are available in the project |
Showing a project
GET /projects/456.[json|xml]
A GET
request will return a representation of project 456
. The representation will be formatted as shown above. The include
query parameter mentioned above is available as well when fetching a single project.
Creating a project
POST /projects.[json|xml]
A POST
request will create a new project. The representation must be formatted as shown above. All parameters which are not marked as read-only can be set during a create request. The identifier can be set as well when creating a project, otherwise it will be automatically derived from the given project name.
Updating a project
PUT /projects/456.[json|xml]
A PUT
request will update project 456
. The representation must be formatted as shown above. All parameters which are not marked as read-only can be set during an update request. Omitted parameters will not be changed.
Deleting a project
DELETE /projects/example.[json|xml]
A DELETE
request will delete the project with the identifier exmaple
.
Time Entries
A sample time entry in XML format:
<time_entry>
<id>12</id>
<project id="5" name="Support"/>
<issue id="186"/>
<user id="5" name="jdoe"/>
<activity id="10" name="Support"/>
<hours>2.0</hours>
<comments/>
<spent_on>2020-03-02</spent_on>
<created_on>2020-03-02T10:31:47Z</created_on>
<updated_on>2020-03-02T10:31:47Z</updated_on>
</time_entry>
Time entries have the following attributes:
Attribute | Required | Read-only | Description |
---|---|---|---|
id | no | yes | Internal identifier of this time entry |
project | yes | no | Project the time entry belongs to. In case an issue is present, this is optional and inferred from the issue. |
issue | yes | no | Issue this time entry belongs to. Optional when a project is given. In this case the time is considered to be booked on the project, and not on a specific issue. |
user | yes | no | User who spent the time |
activity | yes | no | Activity - use this to categorize time entries. The list of activities is configured in Administration / Enumerations |
hours | yes | no | Spend hours in decimal format |
comments | no | no | Optional description (i.e., what was done). 1024 characters maximum |
spent_on | yes | no | Date the time was spent on |
custom_fields | no | no | see custom fields |
created_on | no | yes | Date and time at which the time entry was created; formatted as ISO 8601 timestamp |
updated_on | no | yes | Date and time at which the time entry was last updated; formatted as ISO 8601 timestamp |
Listing time entries
GET /time_entries.[json|xml]
A GET
request will return a paginated list of time_entries. The following query parameters can be used to filter the result set:
Parameter | Description |
---|---|
project_id | Filter by project; use a numeric id here |
from | Lower bound (inclusive) for the spent_on value in YYYY-MM-DD format |
to | Upper bound (inclusive) for the spent_on value in YYYY-MM-DD format |
Showing a time entry
GET /time_entries/456.[json|xml]
A GET
request will return a representation of time entry 456
. The representation will be formatted as shown above.
Creating a time entry
POST /time_entries.[json|xml]
A POST
request will create a new time entry. The representation must be formatted as shown above. All parameters which are not marked as read-only can be set during a create request.
Updating a time entry
PUT /time_entries/456.[json|xml]
A PUT
request will update time entry 456
. The representation must be formatted as shown above. All parameters which are not marked as read-only can be set during an update request. Omitted parameters will not be changed.
Deleting a time entry
DELETE /time_entries/456.[json|xml]
A DELETE
request will delete time entry 456
.
Repositories
A sample repository in JSON format:
{
"repository": {
"id": 123,
"project": { "id": 42, "name": "My project" },
"identifier": "",
"is_default": true,
"scm": "git",
"ssh_url": "git@example.plan.io:my-project.git",
"branches": [
{ "name": "master" },
{ "name": "feature/acme-widgets" }
],
"disksize": 62828372,
"created_on": "2016-07-14T17:43:34Z"
}
}
A sample repository in XML format:
<repository>
<id>124</id>
<project id="42" name="My project"/>
<identifier>documents</identifier>
<is_default>false</is_default>
<scm>subversion</scm>
<is_mirrored>false</is_mirrored>
<svn_url>https://example.plan.io/svn/my-project.documents</svn_url>
<disksize>2168821</disksize>
<created_on>2016-04-16T11:28:59Z</created_on>
</repository>
Access the hosted Git and Subversion repositories within your Planio account via this endpoint.
Repository representations are composed as follows:
Attribute | Required | Read-only | Description |
---|---|---|---|
id | no | yes | Internal identifier of this repository |
project | no | yes | Project to which the repository belongs, identified by its id and name |
identifier | no | yes | Identifier of the repository; one repository per project can have an empty identifier |
is_default | no | yes | Specifies whether the repository is the project’s default repository |
scm | no | yes | Source control management system used for this repository, either git or subversion |
ssh_url | no | yes | SSH clone URL to access the repository; only if scm is git |
svn_url | no | yes | Subversion HTTPS URL to access the repository; only if scm is subversion |
is_mirrored | no | yes | If this repository is mirrored from an external host; only if scm is git |
mirror_from_url | no | yes | URL of the externally hosted repository from where this repository is mirrored; only if is_mirrored is true |
branches | no | yes | Array of available branches for this repository |
disksize | no | yes | The size of the repository in bytes |
created_on | no | yes | Date and time at which the repository was created |
The Repositories endpoint is a Planio addition and is not present in the standard Redmine API.
Listing repositories
This will return a list of repositories you have access to:
curl 'https://example.plan.io/repositories.json' \
-H 'X-Redmine-API-Key: 0123456789abcdef'
GET /repositories.[json|xml]
A GET
request will return a paginated list of repositories.
Possible parameters:
Parameter | Default | Description |
---|---|---|
include | — | Using branches as a value will return the list of branches |
scm | — | Only list repositories of the specified SCM type; possible values are git or subversion or nothing at all |
limit | 25 | The number of objects to return in the response, maximum is 100 |
offset | 0 | The index of the first element to return |
Companies
A sample company in XML format:
<company>
<id>12</id>
<name>Planio GmbH</name>
<address1>Rudolfstr. 14</address1>
<address2/>
<zip>10245</zip>
<town>Berlin</town>
<state/>
<country>Germany</country>
<country_code>de</country_code>
<issue_priority id="5" name="High"/>
<mails type="array">
<mail>support@plan.io</mail>
</mails>
<phones type="array">
<phone value="+49 (30) 577 0000-0" kind="work"/>
</phones>
<projects type="array">
<project id="5" name="Support"/>
</projects>
<created_on>2016-12-19T11:13:34Z</created_on>
<updated_on>2016-12-19T11:14:01Z</updated_on>
</company>
Company representations can have the following attributes:
Attribute | Required | Read-only | Description |
---|---|---|---|
id | no | yes | Internal identifier of this company |
name | yes | no | Company name |
address1 | no | no | Address line 1 |
address2 | no | no | Address line 2 |
zip | no | no | Postal code |
town | no | no | City or town |
state | no | no | State |
country | no | yes | Full country name |
country_code | no | no | ISO 3166-1 alpha-2 country code |
issue_priority | no | no | Priority given to new issues created with this company identified by its id and name ; see Issue priorities |
mails | no | no | Array of emails associated with this company |
phones | no | no | Array of phone numbers for this company identified by their value and kind |
projects | no | no | Array of projects this company can be used in identified by their id and name ; see Projects |
custom_fields | no | no | see custom fields |
created_on | no | yes | Date and time at which the company was created; formatted as ISO 8601 timestamp |
updated_on | no | yes | Date and time at which the company was last updated; formatted as ISO 8601 timestamp |
Listing companies
GET /companies.[json|xml]
A GET
request will return a paginated list of companies.
Possible parameters:
Parameter | Default | Description |
---|---|---|
name | — | Filter by name, address, mails and phone of the company, first and last name, mails and phone of contacts, and searchable custom fields |
project_id | — | Filter by project; use a numeric id here |
limit | 25 | The number of objects to return in the response, maximum is 100 |
offset | 0 | The index of the first element to return |
sort | name | Specify an attribute you’d like to sort by; append :desc to reverse the sort order. Possible attributes for sorting are name , project , lastname , created_on , updated_on . |
Showing a company
GET /companies/123.[json|xml]
A GET
request will return a representation of company 123
. The representation will be formatted as shown above.
Creating a company
POST /companies.[json|xml]
A POST
request will create a new company. The representation must be formatted as shown above. All parameters which are not marked as read-only can be set during a create request.
Associated objects have to be specified using the parameter name with an _id
or _ids
suffix, i.e. issue_priority_id
and project_ids
.
Updating a company
PUT /companies/123.[json|xml]
A PUT
request will update company 123
. The representation must be formatted as shown above. All parameters which are not marked as read-only can be set during an update request. Omitted parameters will not be changed.
Associated objects have to be specified using the parameter name with an _id
or _ids
suffix, i.e. issue_priority_id
and project_ids
.
Deleting a company
DELETE /companies/123.[json|xml]
A DELETE
request will delete company 123
.
Contacts
A sample contact in XML format:
<contact>
<id>15</id>
<projects>
<project id="5" name="Support" identifier="support"/>
</projects>
<company id="12" name="Planio GmbH"/>
<firstname>Louise</firstname>
<lastname>Engel</lastname>
<gender>false</gender>
<mails>
<mail>support@plan.io</mail>
</mails>
<phones>
<phone kind="work">+49 (30) 577 0000-0</phone>
</phones>
<created_on>2016-12-19 15:25:03 UTC</created_on>
<updated_on>2016-12-19 15:25:35 UTC</updated_on>
</contact>
Contact representations can have the following attributes:
Attribute | Required | Read-only | Description |
---|---|---|---|
id | no | yes | Internal identifier of this contact |
projects | no | yes | Array of projects this contact can be used in identified by their id , name and identifier ; see Projects |
company | yes | no | Company this contact is associated to identified by its id and name ; see Companies |
firstname | no | no | First name of the contact |
lastname | no | no | Last name of the contact |
gender | no | no | Gender of the contact, true is male, false is female, can be left blank |
mails | no | no | Array of emails associated with this contact |
phones | no | no | Array of phone numbers for this contact identified by their value and kind |
custom_fields | no | no | see custom fields |
created_on | no | yes | Date and time at which the contact was created; formatted as ISO 8601 timestamp |
updated_on | no | yes | Date and time at which the contact was last updated; formatted as ISO 8601 timestamp |
Listing contacts
GET /companies/123/contacts.[json|xml]
A GET
request will return a paginated list of contacts associated with the company 123
.
Showing a contact
GET /companies/123/contacts/456.[json|xml]
A GET
request will return a representation of contact 456
of company 123
. The representation will be formatted as shown above.
Creating a contact
POST /companies/123/contacts.[json|xml]
A POST
request will create a new contact of company 123
. The representation must be formatted as shown above. All parameters which are not marked as read-only can be set during a create request.
Updating a contact
PUT /companies/123/contacts/456.[json|xml]
A PUT
request will update contact 456
of company 123
. The representation must be formatted as shown above. All parameters which are not marked as read-only can be set during an update request. Omitted parameters will not be changed.
Deleting a contact
DELETE /companies/123/contacts/456.[json|xml]
A DELETE
request will delete contact 456
of company 123
.
Storage Files
A sample file representation with version history
{
"storage_file": {
"id": 123,
"name": "photo.jpg",
"created_on": "2019-01-19T08:00:17Z",
"creator": { "id": 5, "name": "John Doe" },
"updated_on": "2019-01-26T01:22:38Z",
"updater": { "id": 5, "name": "John Doe" },
"webdav_url": "https://example.plan.io/dav/my_project/photo.jpg",
"parent": { "id": 1 },
"filesize": 1360573,
"content_type": "image/jpeg",
"location": "https://example.plan.io/projects/my_project/storage/files/123.json",
"download_url": "https://example.plan.io/projects/my_project/storage/files/123/download/photo.jpg",
"versions": [
{
"version": 1,
"creator": { "id": 5, "name": "John Doe" },
"created_on": "2019-01-19T08:00:18Z",
"download_url": "https://example.plan.io/projects/my_project/storage/files/123/download/1/photo.jpg"
},
{
"version": 2,
"creator": { "id": 5, "name": "John Doe" },
"created_on": "2019-01-19T09:10:10Z",
"download_url": "https://example.plan.io/projects/my_project/storage/files/123/download/2/photo.jpg"
},
{
"version": 2,
"creator": { "id": 5, "name": "John Doe" },
"created_on": "2019-01-26T01:22:38Z",
"download_url": "https://example.plan.io/projects/my_project/storage/files/123/download/3/photo.jpg"
}
]
}
}
Files in Planio Storage are represented with the following attributes:
Attribute | Required | Read-only | Description |
---|---|---|---|
id | no | yes | Internal identifier of this file |
name | yes | no | File name |
created_on | no | yes | Date and time at which the file was created; formatted as ISO 8601 timestamp |
creator | no | yes | User who created the file, identified by their id and name ; see Users |
updated_on | no | yes | Date and time at which the file was last updated; formatted as ISO 8601 timestamp |
updater | no | yes | User who last updated the file, identified by their id and name ; see Users |
webdav_url | no | yes | URL for accessing this file via WebDAV |
parent | yes | yes | Parent folder of this file identified by it’s id . |
filesize | no | yes | File size in Bytes |
content_type | no | yes | Content type of the file |
location | no | yes | Canonical URL for retrieval of this file via the API |
download_url | no | yes | Canonical URL for download of the actual file via HTTP GET. |
Retrieving file details
GET /projects/<identifier>/storage/files/<id>.json
A GET
request will return a representation of the file with the given ID. Use the include
parameter with a value of versions
to request additional information about the version history of the file:
GET /projects/my_project/storage/files/123.json?include=versions
Sample payload to create a new file:
{
"storage_file": {
"parent_id": 1,
"upload":{
"token": "66.228122552730a9c413d523e93a7bcb51",
"filename": "some_file.txt",
"content_type": "text/plain"
}
}
}
Creating a new file
Creating a file in Planio Storage is a two step process:
First, the file has to be uploaded using the standard file uploads endpoint. Then, issue a POST request specifying the parent folder id and an upload element, which at least has to contain the token value referencing the previously uploaded file and the filename.
POST https://example.plan.io/projects/<identifier>/storage/files.json
Sample payload to change a file name:
{ "storage_file": { "name": "updated name" } }
Updating file metadata
To change a file’s name, use the PATCH method on the file’s location. This works exactly the same as with folders.
PATCH /projects/<identifier>/storage/files/<id>.json
The response will be a 200 OK along with a JSON representation of the updated file. In case of validation errors (i.e. a duplicate or empty name), the response will have a status code of 422 and carry a JSON body explaining the error.
Sample payload for creating a new version:
{
"storage_file_version": {
"description": "An optional description for this version",
"upload": {
"token": "66.228122552730a9c413d523e93a7bcb51",
"content_type": "text/plain"
}
}
}
Updating file contents / creating a new version
The actual file content can be updated by creating a new version.
First, upload the actual file using the uploads API as described above. Again, use the resulting token to associate the new version with the file you just uploaded.
POST /projects/<identifier>/storage/files/<id>/versions.json
The description
attribute is optional and can be used to describe the changes present in this new version. The content_type
is optional as well, if present, it overrides the content type that was automatically determined when the file was uploaded.
If the version was created, the response is an empty 201 Created response, with a Location
header pointing to the JSON representation of the file that was just updated.
Deleting a file
Deletion is done via a DELETE
request to the file’s location.
DELETE /projects/<identifier>/storage/files/<id>.json
The response will have an HTTP 200 (OK) status code and no body if the request succeeded. In case of an error an error message will be returned along with an appropriate status code.
Storage Folders
Folders are represented with the following attributes:
Attribute | Required | Read-only | Description |
---|---|---|---|
id | no | yes | Internal identifier of this folder |
name | yes | no | Folder name |
created_on | no | yes | Date and time at which the folder was created; formatted as ISO 8601 timestamp |
creator | no | yes | User who created the folder, identified by their id and name ; see Users |
updated_on | no | yes | Date and time at which the folder was last updated; formatted as ISO 8601 timestamp |
updater | no | yes | User who last updated the folder, identified by their id and name ; see Users |
webdav_url | no | yes | URL for accessing this folder via WebDAV |
parent | yes | yes | Parent folder of this folder, identified by it’s id . |
location | no | yes | Canonical URL for retrieval of this folder via the API |
Sample payload for creating a folder
{
"storage_folder": {
"parent_id": 1,
"name": "new folder",
}
}
Creating a folder
To create a new folder, issue a POST
request containing the id of the parent folder, as well as the desired folder name.
POST /projects/<identifier>/storage/folders.json
A successful creation will result in a response with status code 201 (Created) along with a JSON representation of the new folder.
Sample payload for updating a folder
{ "storage_folder": { "name": "The new name" } }
Updating a folder
For updating a folder, issue a PATCH request to it’s location. It is not possible to update Project root folders since these are tied to their respective project. As of now, only the folder name can be modified through this method.
PATCH /projects/<identifier>/storage/folders/<id>.json
The response will be a 200 OK along with a JSON representation of the updated folder. In case of validation errors (i.e. a duplicate or empty name), the response will have a status code of 422 along with a JSON body explaining the error.
Deleting a folder
To delete a folder including any existing children, issue a DELETE
to it’s location. It is not possible to delete project root folders.
DELETE /projects/<identifier>/storage/folders/<id>.json
The response will have an HTTP 200 (OK) status code and no body if the request succeeded. In case of an error a hopefully helpful error will be returned along with an appropriate status code.