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, latest news | 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) |
![]() |
![]() |
![]() |
![]() |
Can change own username/password? |
![]() |
![]() |
![]() |
![]() |
Can read feed data? |
![]() |
![]() |
![]() |
![]() |
Can read other data? |
![]() |
![]() |
![]() |
![]() |
Can write data? |
![]() |
![]() |
![]() |
![]() |
Copes with self-managed accounts |
![]() |
![]() |
![]() |
![]() |
Can use different token per application? |
![]() |
![]() |
![]() |
![]() |
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.
GET /news.json
GET /news/12345.json
GET /news.rss
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.
// 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.
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.
// 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.
GET /accounts/check_username_validity?username=user&format=json
// 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.
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
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.
// 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.
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.
// 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:
GET /admin/rotas.json
Shifts
More information about a shift can also be found, this is currently only available in a JSON format.
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.
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.
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.
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.
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 }
// 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.