NAV
shell

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 AvatarAdministrationSettingsAuthentication 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 AvatarAdministrationSettingsApplications 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

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 avatarMy 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:

  1. 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).
  2. 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.

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 set Department to Sales:

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:

  1. Upload the file, receive a token
  2. 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 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
mail 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="&#x2713;" />

  <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.