API

Three Rings provides an API with which selected data can be accessed using third-party computer programs. For help with using the API, please get in touch with the support team.

Fundamentals

All Three Rings resources are made available exclusively over HTTPS underneath the https://www.3r.org.uk/ domain. HTTP/2 is supported and you are encouraged to use it if you are able, as this provides performance benefits. The domain itself is available via both IPv4 and IPv6.

Where possible, a REST architecture is used. Most resources are made available only in JSON format (however, there are a few exceptions).

Authentication

Three Rings accepts authentication requests in four different ways: session login (either via a username and password or a third-party like a Google Account), feed key, HTTP basic authentication, and API key. When accessing the Three Rings API you will almost always and to use either a session log or an API key. A session login is best when writing a browser plugin or bookmarklet; an API key is best when writing a standalone application. A summary of the different authentication methods can be found below.

Regardless of the authentication method chosen, you can never gain access to resources that the user could not gain access to via a conventional session login. E.g. if a volunteer is restricted from being able to see a particular rota, logging in with an API key will not allow this restriction to be circumvented.

Session Login Feed Key HTTP Basic API Key
Best for Browser plugins/bookmarklets Feed subscriptions Where API key not an option Standalone applications
Authentication Account username and password (then use cookie) Volunteer feed key Account username and password Volunteer API key
Credentials found at Username and password Own 'upcoming shifts' page Username and password Put /api on end of own Directory URL
Limitations Unless plugin/bookmarklet, must implement cookie handling Can only access feeds (e.g. upcoming shifts iCalendar feed) Need to store username/password (not recommended) or else enter on each execution Minor access limitations
Stateless (no need to implement cookies) No Yes Yes Yes
Can change own username/password? Yes No Yes No
Can read feed data? Yes Yes Yes Yes
Can read other data? Yes No Yes Yes
Can write data? Yes No Yes Yes
Copes with self-managed accounts Yes Yes No Yes
Can use different token per application? No No No Yes

Passing Authentication Tokens

The mechanism by which authentication tokens are passed depends upon the strategy employed. For session logins, it is necessary either to hijack an existing session (e.g. via a plugin or bookmarklet) or else to implement authentication and handling of the resulting cookies: this can be implemented using the --cookie-jar switch in cURL, for example.

Specific examples for HTTP Basic and API Keys are shown below.

HTTP Basic Authentication

This is the preferred method of authentication only where API Keys are not an option. If you're not certain, it's preferable to use an API key as described below. Note that HTTP Basic Authentication does not function when using a self-managed account.

The mechanism for HTTP Basic Authentication is described in RFC 1945 but can be simply described as follows: pass an Authorization: HTTP header containing the word 'Basic', followed by a space, followed by the Base64-encoded rendition of the username and password to use, separated by a colon. Your library may provide this functionality on your behalf. For example, the following request uses cURL to request the Directory in CSV format using the username 'myUsername' and the password 'myPassword':

curl --user myUsername:myPassword https://www.3r.org.uk/directory.csv

Note the Three Rings server will not prompt your application with a WWW-Authenticate: challenge header - you must send HTTP Basic credentials without this prompt. Be aware that some libraries need to be specially-configured to cope with this.

API Key Authentication

The preferred strategy for a standalone application to authenticate with Three Rings is via an API key. A user can request as many API keys as they wish by going to their own Directory page (e.g. by clicking on their own name in the top right after logging in to an organisation where they volunteer) and then appending /api to the URL. This way, they can issue a different API key to each application they wish to grant access to their identity, and monitor the use of each independently (and remove their access, should they need to, without having to change their password).

To authenticate using an API key, pass an Authorization: HTTP header containing the word 'APIKEY', followed by a space, followed by the API key to use. For example, the following request uses cURL to request the Directory in CSV format using the API key 'HRR2FT4JPgdsZGyXz4XvVgtt':

curl -H "Authorization: APIKEY HRR2FT4JPgdsZGyXz4XvVgtt" https://www.3r.org.uk/directory.csv

News

News items appear on the Overview page and can come from news managers at the organisation to which the volunteer belongs or, under some circumstances, from related organisations.

Sample requests
GET /news.json
GET /news/12345.json

Each news item exposes an id number, title, body, url, priority, 'stickiness', and details of when it was last created/updated and by whom. Requesting non-existent news returns a 404. Requesting news while not logged in as a user who can see that news returns a 403.

Sample code
// pops up alert boxes containing the title of each visible news item, if the user is logged in and can see news
jQuery.getJSON('/news.json', function(data, textStatus, jqXHR){
  data.news_items.forEach(function(news_item){
    alert(news_item.title);
  });
}).fail(function(jqXHR, textStatus, error){
  alert('Failed - ' + error + ' - perhaps not logged in to Three Rings?');
});
#!/usr/bin/env ruby
# use the rest-client and json gems to streamline
require 'rest-client'
require 'json'

# credentials:
USERNAME, PASSWORD = 'username', 'password'

response = JSON.load(RestClient.get("https://\#{USERNAME}:\#{PASSWORD}@www.3r.org.uk/news.json"))
response['news_items'].each do |news_item|
  puts news_item['title']
end
<?php
// Credentials go here:
define('USERNAME', 'demo_admin');
define('PASSWORD', 'pa55word=admin');

// We're using HTTPFul, from http://phphttpclient.com, to make this easier
include('./httpful.phar');

$response = \Httpful\Request::get("https://www.3r.org.uk/news.json")->authenticateWith(USERNAME, PASSWORD)->send();
foreach($response->body->news_items as $news_item){
  echo $news_item->title, "<br />\n";
}
?>
#!/usr/bin/env python
import urllib, json

USERNAME, PASSWORD = 'username', 'password'
url = "https://"+USERNAME+":"+PASSWORD+"@3r.org.uk/news.json"

response = urllib.urlopen(url)
data = json.loads(response.read())

for news_item in data['news_items']:
  print news_item['title']

Events

Events appear on the Overview page and Rota and can come from event managers at the organisation to which the volunteer belongs or, under some circumstances, from related organisations.

Sample requests
GET /events.json
GET /events.json?start_date=2018-04-12
GET /events.json?start_date=2018-04-12&end_date=2018-04-14
GET /events/12345.json

Each event exposes a name, description, type, date, and details of when it was last created/updated and by whom. Dynamically-generated virtual events, such as birthdays, do not have an id number nor url, but events that exist in the database in their own right do have these attributes. Note that the returned events collection will be empty if there are no upcoming events.

Sample code
// pops up alert boxes containing the name of each visible event, if the user is logged in and can see events
jQuery.getJSON('/events.json', function(data, textStatus, jqXHR){
  data.events.forEach(function(event){
    alert(event.name);
  });
  if(jQuery.isEmptyObject(data.events)){
    alert("No upcoming events");
  }
}).fail(function(jqXHR, textStatus, error){
  alert('Failed - ' + error + ' - perhaps not logged in to Three Rings?');
});
#!/usr/bin/env ruby
# use the rest-client and json gems to streamline
require 'rest-client'
require 'json'

# credentials:
USERNAME, PASSWORD = 'username', 'password'

response = JSON.load(RestClient.get("https://\#{USERNAME}:\#{PASSWORD}@www.3r.org.uk/events.json"))
response['events'].each do |event|
  puts "\#{event['name']} : \#{event['description']}"
end
<?php
// Credentials go here:
define('USERNAME', 'username');
define('PASSWORD', 'password');

// We're using HTTPFul, from http://phphttpclient.com, to make this easier
include('./httpful.phar');

$response = \Httpful\Request::get("https://www.3r.org.uk/events.json")->authenticateWith(USERNAME, PASSWORD)->send();
foreach($response->body->events as $event){
  echo $event->name, "<br />\n";
}
?>
#!/usr/bin/env python
import urllib, json
import dateutil.parser

USERNAME, PASSWORD = 'username', 'password'
url = "https://"+USERNAME+":"+PASSWORD+"@3r.org.uk/events.json"

response = urllib.urlopen(url)
data = json.loads(response.read())

for event in data['events']:
  print event['name'] + " : " + event['description']

Accounts

In Three Rings Accounts and Volunteers are conceptually different things. Accounts have an associated username and password and are used to log in to Three Rings. Volunteers are the instance of a user at an organisation, for each organisation a user is associated with they will be represented by a different Volunteer. A self-managed Account can have more than one Volunteer associated to it. This allows users to have different properties for each organisation but still authenticate using the same username and password.

When creating a volunteer a unique username must be used, because usernames belong to accounts and not volunteers this must be checked through accounts.

Sample requests
GET /accounts/check_username_validity?username=user&format=json
Sample code
// pops up an alert box saying whether the username is available
jQuery.getJSON('/accounts/check_username_validity?username=demo_admin&format=json', function(data, textStatus, jqXHR){
  if(data.valid) {
    alert('Username is available');
  } else {
    alert('You can\'t have that username: ' + data.message);
  }
}).fail(function(jqXHR, textStatus, error){
  alert('Failed - ' + error + ' - perhaps not logged in to Three Rings?');
});
# put the URL inside quotation marks to avoid undesired behaviour in some command lines
curl -k -u USERNAME:PASSWORD "https://www.3r.org.uk/accounts/check_username_validity?username=user&format=json"

Note that it is not possible to change the details of the currently-logged-in Account (though it is possible to change the details of any Volunteers associated with it) with authenticating using an API key.

Volunteers (Directory)

Volunteers are listed and viewed via the Directory. Only volunteers from the same organisation as the currently logged-in volunteer are accessible. The individual property values (details) of a volunteer that are available may vary depending upon the permissions granted to the currently logged-in volunteer by their organisation, or upon the properties used. The 'code' assigned to a property (e.g. "email" for the volunteer's email address) is unique and can be relied upon to be associated with a particular property.

For convenience and for historical reasons, the volunteers API is accessible via additional formats, including CSV and XLS (for the full volunteer list) and 'vCard' VCF format (for individual volunteers - currently only accessible to volunteers with admin tab access at the organisation).

It is possible to pass additional parameters to the directory 'list' in order to filter the returned volunteers by role, to return only sleepers, etc. Note that not all formats are available for all kinds of query: some experimentation may be required. Note that when requesting information about a particular volunteer, the username or ID number can be used as the key, and the format must be specified in ?format=[format] syntax (see samples below) - this is because usernames are permitted to contain full stops.

Sample requests
GET /directory.json
GET /directory.csv
GET /directory.xls
GET /directory/view_by_role/1234.csv
GET /directory.json?volunteer_filter=sleepers
GET /directory/12345?format=vcf
GET /directory/new.json
POST /directory/create.json
<>%p Each volunteer exposes an ID number (note that this will differ for each identity of a person who volunteers at multiple organisations), account, and whether or not they're a support, and when it was last created/updated and by whom.

When requesting an individual volunteer, a collection of 'volunteer_properties' is returned - this provides all of the accessible property/value pairs associated with that volunteer: e.g. showing what their telephone number, email address, or date of birth is.

Sample code
// pops up an alert box identifying the volunteer whose account was most-recently created
jQuery.getJSON('/directory.json', function(data, textStatus, jqXHR){
  var latest_created_at = '0000-00-00';
  var latest_created_username;
  data.volunteers.forEach(function(volunteer){
    if(volunteer.created_at && volunteer.created_at > latest_created_at){
      latest_created_at = volunteer.created_at;
      latest_created_username = volunteer.account.username;
    }
  });
  alert('The most-recently created volunteer account was ' + latest_created_username + ' at ' + latest_created_at + '.');
}).fail(function(jqXHR, textStatus, error){
  alert('Failed - ' + error + ' - perhaps not logged in to Three Rings?');
});

You can use the API to create a new volunteer. To create a new volunteer use /directory/new.json to get an empty JSON volunteer object. This object will vary depending on the properties that are enabled at your organisation, so it's a good idea to re-fetch the JSON stub before adding every set of new volunteers. Once you get the empty object, fill in the properties and send it via POST to /directory/create.json with the name volunteers in the payload: you can create an array of these filled in volunteer objects, to add a group of volunteers to Three Rings at once. The response is a JSON object that has a success array and an errors array. The errors array contains the volunteer objects that have not been added to Three Rings: each object will have an errors attribute explaining why adding the volunteer failed. The success array contains the volunteer objects that have been added to Three Rings: each object will have an errors attribute listing the properties that could not be added wth an explanation.

Rotas

Rotas represent collection of shifts that volunteers can sign up to. Each rota can relate to a specific type of shift such as "Day Shift" or "Shift Leader". The request can either return a csv or json format (see examples below). Note in order to export the rota you must have permission to view the stats of your organisation.

Sample requests
GET /stats/export_rotas.json?
GET /stats/export_rotas.json?filter_rotas=1234
GET /stats/export_rotas.json?filter_rotas=multiple&filter_rota_list[5687]=1&filter_rota_list[5688]=1
GET /stats/export_rotas.json?start_date=2019-02-15&end_date=2019-02-16&filter_rotas=1234

When a start_date and end_date is specified, the request returns all shifts, including the names of volunteers on those shifts within the specified period for the specified rotas. If no rotas are included then the request returns shifts from all rotas in a specified period. If a start date is not given, the request will return all shifts for the current day until the given end date. If the end date is not given then the request returns all the shifts on the start date.

Sample code
// pops up alert boxes containing the names of volunteers of the first shift from one day ago.
jQuery.getJSON('/stats/export_rotas.json', function(data, textStatus, jqXHR){
    shift = data.shifts[0]
    var volunteers = shift.volunteers;
    var vol_list = "";
    for (i = 0; i< volunteers.length; i++){
      vol_list += volunteers[i].name + " ";
    }
    alert(shift.title+vol_list);

}).fail(function(jqXHR, textStatus, error){
  alert('Failed - ' + error + ' - perhaps not logged in to Three Rings?');
});
#!/usr/bin/env ruby
# use the rest-client and json gems to streamline
require 'rest-client'
require 'json'
require 'date'

# credentials:
USERNAME, PASSWORD = 'username', 'password'

response = JSON.load(RestClient.get("https://\#{USERNAME}:\#{PASSWORD}@www.3r.org.uk/stats/export_rotas.json?start_date=#{DateTime.now.yesterday.strftime('%Y-%m-%d')}"))
response['shifts'].each do |shift|
  vol_list=""
  shift['volunteers'].each do |volunteer|
    vol_list += volunteer['name'] + " "
  end
  puts "\#{shift['rota']} \#{shift['title']}  \#{Date.parse(shift['start_datetime']).strftime}   :   \#{vol_list}"
end
#!/usr/bin/env python
import urllib, json
import dateutil.parser

USERNAME, PASSWORD = 'username', 'password'
url = "https://"+USERNAME+":"+PASSWORD+"@3r.org.uk/stats/export_rotas.json?start_date=#{DateTime.now.yesterday.strftime('%Y-%m-%d')}"

response = urllib.urlopen(url)
data = json.loads(response.read())

for shift in data['shifts']:
  vol_list = ""
  for volunteer in shift['volunteers']:
    vol_list += volunteer['name'] + " "
  d = dateutil.parser.parse(shift['start_datetime'])
  print shift['title'] + d.strftime("%I:%M %p %a %d %b %y") + " : " + vol_list

It is also possible, if authenticated using an account with access to the Admin tab, to get a list of all rotas without their shifts:

Sample requests
GET /admin/rotas.json

Shifts

More information about a shift can also be found, this is currently only available in a JSON format.

Sample requests
GET /shift.json
GET /shift.json?shift_id=123456
GET /shift.json?start_date=2018-04-12
GET /shift.json?start_date=2018-04-12&end_date=2018-04-14

When a start_date and end_date is specified, the request returns all shifts within the specified period. If a start date is not given then the request will return all shifts for the given rotas from the current day until the given end date. If the end date is not given then the request returns all shifts up until 7 days after the start date.

Roles

If authenticated using an account with access to the Admin tab, a list of all of the organisation's roles can be requested.

Sample requests
GET /admin/roles.json

If you're looking for volunteers with a particular role, see: Volunteers

Properties

If authenticated using an account with access to the Admin tab, a list of all of the properties used by an organisation, and the access restrictions applied to each, can be requested.

Sample requests
GET /admin/properties.json

Relationships

If authenticated using an account with access to the Admin tab, a list of all of the relationship types defined by an organisation.

Sample requests
GET /admin/relationships.json

Inactivity Types

If authenticated using an account with access to the Admin tab, a list of all of the inactivity types defined by an organisation and their rules.

Sample requests and responses
GET /admin/inactivity.json
GET /admin/inactivity.json

This returns an array of objects in JSON format, each object contains information about one activity type, here is an example of one of the objects.

Object {
  id: integer // The unique ID of the inactivity type
  org_id: integer // The unique ID of the organisation the inactivity type belongs to
  name: String // The name of the inactivity type
  icon: String // The string identifier of the icon used for the inactivity type
  email_as_active: boolean // If false users that have the inactivity type will not recieve comms messages, unless over-ridden on the comms page
  self_managers_can_create: boolean // If true users with Directory: Self-Manage permission can set the inactivity type themselves
  all_rotas: boolean // Can the inactivity type be used on all rotas
  creator_id: integer // The unique ID of the volunteer that created the inactivity type
  updater_id: integer // The unique ID of the volunteer that last updated the inactivity type. If the inactivity has not been updated this will be the same as creator
  created_at: String // The datetime string (in ISO 8601 format) of when the inactivity type was created
  updated_at: String // The datetime string (in ISO 8601 format) of when the inactivity type was updated. If the inactivity has not been updated this will be the same as creator
  max_duration: integer or null // The maximum number of days an inactivity period can be, if null there is no maximum
  position: integer // This is the order the inactivities appear in the admin panel
}
Sample code
// pops up alert box containing a list of the organisations inactivities.
jQuery.getJSON('/admin/inactivity.json', function(data, textStatus, jqXHR){
    inactivity = data;
    var inactivity_list = "";
    for (i = 0; i < inactivity.length; i++){
      inactivity_list += "- " + inactivity[i].name + "\n";
    }
    alert(inactivity_list);
}).fail(function(jqXHR, textStatus, error){
  alert('Failed - ' + error + ' - perhaps not logged in to Three Rings?');
});
#!/usr/bin/env ruby
# use the rest-client and json gems to streamline
require 'rest-client'
require 'json'

# credentials:
USERNAME, PASSWORD = 'username', 'password'

response = JSON.load(RestClient.get("https://\#{USERNAME}:\#{PASSWORD}@www.3r.org.uk/admin/inactivity.json"))
puts "Name | Active in Comms | All rotas? | Max duration"
response.each do |inactivity|
  max_duration = if inactivity['max_duration'] then inactivity['max_duration'] else "None" end
  puts "\#{inactivity['name']} | \#{inactivity['email_as_active']} | \#{inactivity['all_rotas']} | \#{max_duration}"
end
#!/usr/bin/env python
import urllib, json
import dateutil.parser

USERNAME, PASSWORD = 'username', 'password'
url = "https://"+USERNAME+":"+PASSWORD+"@3r.org.uk/admin/inactivity.json"

response = urllib.urlopen(url)
data = json.loads(response.read())

inactivity_list = ""
for inactivity in data:
    inactivity_list += "- " + volunteer['name'] + "\n"
print inactivity_list

Terms of Use

We recommend that any software designed to extend or enhance the functionality of Three Rings is released under an open-source licence. As a security precaution, we will recommend against the use of any closed-source extensions as a matter of course.

Automated applications must not make excessive demands upon the server. Requests should be limited to no more than 30 in any 60-second period. We reserve the right to block requests by applications that violate this rule, and also the user accounts that use those applications.

We recommend that automated applications set the User-Agent header to one which identifies the application by name and provides the email address of the author, so that we're able to contact them in the event of any problems or concerns.

Plugin authors should ensure that their Three Rings account is subscribed to the 'Release Notes' feed of System News, to ensure that they're informed when API changes occur. They might also consider joining the test team so as to get access to new versions of Three Rings in advance of them becoming generally available.